feat(social): Implement Real Meta API, Webhook Controller, Auto-Reply tests, AI Sentiment analysis and Security Rules
This commit is contained in:
@@ -6,7 +6,7 @@ USER root
|
|||||||
COPY requirements.txt /etc/odoo/
|
COPY requirements.txt /etc/odoo/
|
||||||
|
|
||||||
# Gerekli Python paketlerini kur
|
# Gerekli Python paketlerini kur
|
||||||
RUN pip3 install --no-cache-dir -r /etc/odoo/requirements.txt
|
RUN pip3 install --no-cache-dir --break-system-packages --ignore-installed -r /etc/odoo/requirements.txt
|
||||||
|
|
||||||
# Odoo kullanıcısına geri dön
|
# Odoo kullanıcısına geri dön
|
||||||
USER odoo
|
USER odoo
|
||||||
|
|||||||
@@ -1 +1,3 @@
|
|||||||
from . import models
|
from . import models
|
||||||
|
from . import controllers
|
||||||
|
from . import wizard
|
||||||
|
|||||||
@@ -22,10 +22,32 @@ Features:
|
|||||||
""",
|
""",
|
||||||
'author': 'MyMach',
|
'author': 'MyMach',
|
||||||
'website': 'https://www.mymach.com',
|
'website': 'https://www.mymach.com',
|
||||||
'depends': ['base', 'web', 'crm', 'mail'],
|
'depends': ['base', 'mail', 'social_media', 'utm', 'link_tracker', 'crm', 'base_setup', 'product'],
|
||||||
'data': [
|
'data': [
|
||||||
|
'security/security.xml',
|
||||||
|
'security/ir_rule.xml',
|
||||||
'security/ir.model.access.csv',
|
'security/ir.model.access.csv',
|
||||||
|
'data/social_post_cron.xml',
|
||||||
|
'data/social_competitor_cron.xml',
|
||||||
|
'views/res_config_settings_views.xml',
|
||||||
|
'views/social_analytics_views.xml',
|
||||||
|
'views/social_live_post_views.xml',
|
||||||
|
'views/social_post_views.xml',
|
||||||
|
'wizard/social_ai_wizard_views.xml',
|
||||||
|
'views/social_stream_views.xml',
|
||||||
|
'views/social_media_views.xml',
|
||||||
|
'views/social_auto_reply_views.xml',
|
||||||
|
'views/social_auto_reply_log_views.xml',
|
||||||
|
'views/social_stream_post_kanban_views.xml',
|
||||||
|
'views/social_menu.xml',
|
||||||
|
'views/social_account_views.xml',
|
||||||
|
'views/social_competitor_views.xml',
|
||||||
],
|
],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'mymach_social_nextgen/static/src/css/social_dashboard.css',
|
||||||
|
],
|
||||||
|
},
|
||||||
'installable': True,
|
'installable': True,
|
||||||
'application': True,
|
'application': True,
|
||||||
'auto_install': False,
|
'auto_install': False,
|
||||||
|
|||||||
6
extra-addons/mymach_social_nextgen/api/__init__.py
Normal file
6
extra-addons/mymach_social_nextgen/api/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
from . import meta_api
|
||||||
|
from . import instagram_api
|
||||||
|
from . import linkedin_api
|
||||||
|
from . import x_api
|
||||||
|
from . import youtube_api
|
||||||
|
from . import tiktok_api
|
||||||
36
extra-addons/mymach_social_nextgen/api/instagram_api.py
Normal file
36
extra-addons/mymach_social_nextgen/api/instagram_api.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import requests
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
class InstagramAPI:
|
||||||
|
"""Helper class for direct Instagram OAuth (Basic Display or Native Auth)."""
|
||||||
|
|
||||||
|
AUTH_URL = "https://api.instagram.com/oauth/authorize"
|
||||||
|
TOKEN_URL = "https://api.instagram.com/oauth/access_token"
|
||||||
|
GRAPH_URL = "https://graph.instagram.com"
|
||||||
|
|
||||||
|
def __init__(self, client_id, client_secret, redirect_uri):
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
self.redirect_uri = redirect_uri
|
||||||
|
|
||||||
|
def get_auth_url(self, state):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'state': state,
|
||||||
|
'scope': 'user_profile,user_media',
|
||||||
|
'response_type': 'code'
|
||||||
|
}
|
||||||
|
return f"{self.AUTH_URL}?{urlencode(params)}"
|
||||||
|
|
||||||
|
def exchange_code_for_token(self, code):
|
||||||
|
data = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'code': code
|
||||||
|
}
|
||||||
|
response = requests.post(self.TOKEN_URL, data=data, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
36
extra-addons/mymach_social_nextgen/api/linkedin_api.py
Normal file
36
extra-addons/mymach_social_nextgen/api/linkedin_api.py
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import requests
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
class LinkedInAPI:
|
||||||
|
"""Helper class for LinkedIn API."""
|
||||||
|
|
||||||
|
AUTH_URL = "https://www.linkedin.com/oauth/v2/authorization"
|
||||||
|
TOKEN_URL = "https://www.linkedin.com/oauth/v2/accessToken"
|
||||||
|
|
||||||
|
def __init__(self, client_id, client_secret, redirect_uri):
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
self.redirect_uri = redirect_uri
|
||||||
|
|
||||||
|
def get_auth_url(self, state):
|
||||||
|
params = {
|
||||||
|
'response_type': 'code',
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'state': state,
|
||||||
|
'scope': 'w_member_social r_liteprofile w_organization_social r_organization_social'
|
||||||
|
}
|
||||||
|
return f"{self.AUTH_URL}?{urlencode(params)}"
|
||||||
|
|
||||||
|
def exchange_code_for_token(self, code):
|
||||||
|
data = {
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'redirect_uri': self.redirect_uri
|
||||||
|
}
|
||||||
|
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
|
||||||
|
response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
69
extra-addons/mymach_social_nextgen/api/meta_api.py
Normal file
69
extra-addons/mymach_social_nextgen/api/meta_api.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import requests
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
class MetaAPI:
|
||||||
|
"""Helper class for Meta (Facebook/Instagram) Graph API."""
|
||||||
|
|
||||||
|
AUTH_URL = "https://www.facebook.com/v19.0/dialog/oauth"
|
||||||
|
TOKEN_URL = "https://graph.facebook.com/v19.0/oauth/access_token"
|
||||||
|
GRAPH_URL = "https://graph.facebook.com/v19.0"
|
||||||
|
|
||||||
|
def __init__(self, client_id, client_secret, redirect_uri):
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
self.redirect_uri = redirect_uri
|
||||||
|
|
||||||
|
def get_auth_url(self, state):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'state': state,
|
||||||
|
'scope': 'pages_show_list,pages_read_engagement,pages_manage_posts,instagram_basic,instagram_content_publish',
|
||||||
|
'response_type': 'code'
|
||||||
|
}
|
||||||
|
return f"{self.AUTH_URL}?{urlencode(params)}"
|
||||||
|
|
||||||
|
def exchange_code_for_token(self, code):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'code': code
|
||||||
|
}
|
||||||
|
response = requests.get(self.TOKEN_URL, params=params, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def get_profile_info(self, access_token):
|
||||||
|
"""Kullanıcının yönettiği sayfaları ve hesap ID'lerini çeker."""
|
||||||
|
url = f"{self.GRAPH_URL}/me/accounts"
|
||||||
|
params = {'access_token': access_token}
|
||||||
|
response = requests.get(url, params=params, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def publish_post(self, page_id, access_token, message, link=None):
|
||||||
|
"""Belirtilen sayfada gönderi paylaşır."""
|
||||||
|
url = f"{self.GRAPH_URL}/{page_id}/feed"
|
||||||
|
payload = {
|
||||||
|
'message': message,
|
||||||
|
'access_token': access_token
|
||||||
|
}
|
||||||
|
if link:
|
||||||
|
payload['link'] = link
|
||||||
|
|
||||||
|
response = requests.post(url, data=payload, timeout=15)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
|
|
||||||
|
def refresh_token(self, short_lived_token):
|
||||||
|
"""Kısa ömürlü token'ı uzun ömürlü (60 günlük) token'a çevirir veya süresini uzatır."""
|
||||||
|
params = {
|
||||||
|
'grant_type': 'fb_exchange_token',
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'fb_exchange_token': short_lived_token
|
||||||
|
}
|
||||||
|
response = requests.get(self.TOKEN_URL, params=params, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
38
extra-addons/mymach_social_nextgen/api/tiktok_api.py
Normal file
38
extra-addons/mymach_social_nextgen/api/tiktok_api.py
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import requests
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
class TikTokAPI:
|
||||||
|
"""Helper class for TikTok Developer API / OAuth 2.0."""
|
||||||
|
|
||||||
|
AUTH_URL = "https://www.tiktok.com/v2/auth/authorize/"
|
||||||
|
TOKEN_URL = "https://open.tiktokapis.com/v2/oauth/token/"
|
||||||
|
|
||||||
|
def __init__(self, client_id, client_secret, redirect_uri):
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
self.redirect_uri = redirect_uri
|
||||||
|
|
||||||
|
def get_auth_url(self, state):
|
||||||
|
params = {
|
||||||
|
'client_key': self.client_id,
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'state': state,
|
||||||
|
'scope': 'user.info.basic,video.list,video.upload',
|
||||||
|
'response_type': 'code'
|
||||||
|
}
|
||||||
|
return f"{self.AUTH_URL}?{urlencode(params)}"
|
||||||
|
|
||||||
|
def exchange_code_for_token(self, code):
|
||||||
|
data = {
|
||||||
|
'client_key': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'code': code
|
||||||
|
}
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
}
|
||||||
|
response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
45
extra-addons/mymach_social_nextgen/api/x_api.py
Normal file
45
extra-addons/mymach_social_nextgen/api/x_api.py
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import requests
|
||||||
|
import base64
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
class XAPI:
|
||||||
|
"""Helper class for X (Twitter) OAuth 2.0 API."""
|
||||||
|
|
||||||
|
AUTH_URL = "https://twitter.com/i/oauth2/authorize"
|
||||||
|
TOKEN_URL = "https://api.twitter.com/2/oauth2/token"
|
||||||
|
|
||||||
|
def __init__(self, client_id, client_secret, redirect_uri):
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
self.redirect_uri = redirect_uri
|
||||||
|
|
||||||
|
def get_auth_url(self, state, code_challenge="challenge"):
|
||||||
|
# Note: X requires PKCE. For a complete implementation, a real code_verifier and code_challenge should be generated.
|
||||||
|
params = {
|
||||||
|
'response_type': 'code',
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'scope': 'tweet.read tweet.write users.read offline.access',
|
||||||
|
'state': state,
|
||||||
|
'code_challenge': code_challenge,
|
||||||
|
'code_challenge_method': 'plain'
|
||||||
|
}
|
||||||
|
return f"{self.AUTH_URL}?{urlencode(params)}"
|
||||||
|
|
||||||
|
def exchange_code_for_token(self, code, code_verifier="challenge"):
|
||||||
|
data = {
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'code': code,
|
||||||
|
'code_verifier': code_verifier
|
||||||
|
}
|
||||||
|
auth_string = f"{self.client_id}:{self.client_secret}"
|
||||||
|
b64_auth = base64.b64encode(auth_string.encode('ascii')).decode('ascii')
|
||||||
|
headers = {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded',
|
||||||
|
'Authorization': f'Basic {b64_auth}'
|
||||||
|
}
|
||||||
|
response = requests.post(self.TOKEN_URL, data=data, headers=headers, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
37
extra-addons/mymach_social_nextgen/api/youtube_api.py
Normal file
37
extra-addons/mymach_social_nextgen/api/youtube_api.py
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import requests
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
class YouTubeAPI:
|
||||||
|
"""Helper class for Google/YouTube Data API v3."""
|
||||||
|
|
||||||
|
AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth"
|
||||||
|
TOKEN_URL = "https://oauth2.googleapis.com/token"
|
||||||
|
|
||||||
|
def __init__(self, client_id, client_secret, redirect_uri):
|
||||||
|
self.client_id = client_id
|
||||||
|
self.client_secret = client_secret
|
||||||
|
self.redirect_uri = redirect_uri
|
||||||
|
|
||||||
|
def get_auth_url(self, state):
|
||||||
|
params = {
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'redirect_uri': self.redirect_uri,
|
||||||
|
'response_type': 'code',
|
||||||
|
'scope': 'https://www.googleapis.com/auth/youtube.upload https://www.googleapis.com/auth/youtube.readonly',
|
||||||
|
'access_type': 'offline',
|
||||||
|
'prompt': 'consent',
|
||||||
|
'state': state
|
||||||
|
}
|
||||||
|
return f"{self.AUTH_URL}?{urlencode(params)}"
|
||||||
|
|
||||||
|
def exchange_code_for_token(self, code):
|
||||||
|
data = {
|
||||||
|
'grant_type': 'authorization_code',
|
||||||
|
'code': code,
|
||||||
|
'client_id': self.client_id,
|
||||||
|
'client_secret': self.client_secret,
|
||||||
|
'redirect_uri': self.redirect_uri
|
||||||
|
}
|
||||||
|
response = requests.post(self.TOKEN_URL, data=data, timeout=10)
|
||||||
|
response.raise_for_status()
|
||||||
|
return response.json()
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
from . import main
|
||||||
|
from . import webhook_controller
|
||||||
76
extra-addons/mymach_social_nextgen/controllers/main.py
Normal file
76
extra-addons/mymach_social_nextgen/controllers/main.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
import werkzeug
|
||||||
|
|
||||||
|
class SocialMediaController(http.Controller):
|
||||||
|
|
||||||
|
@http.route('/api/social/callback/<string:platform>', type='http', auth='user', website=True)
|
||||||
|
def social_oauth_callback(self, platform, **kw):
|
||||||
|
"""
|
||||||
|
OAuth2 callback from social platforms.
|
||||||
|
Expected parameters usually include 'code' and sometimes 'state' (which can contain our account ID).
|
||||||
|
"""
|
||||||
|
code = kw.get('code')
|
||||||
|
state = kw.get('state')
|
||||||
|
error = kw.get('error')
|
||||||
|
|
||||||
|
if error:
|
||||||
|
return request.render('http_routing.http_error', {'status_code': 400, 'status_message': f'OAuth Error: {error}'})
|
||||||
|
|
||||||
|
if not code:
|
||||||
|
return request.render('http_routing.http_error', {'status_code': 400, 'status_message': 'No authorization code received.'})
|
||||||
|
|
||||||
|
# Typically, state contains the ID of the social.account record we are trying to link.
|
||||||
|
account_id = None
|
||||||
|
try:
|
||||||
|
if state:
|
||||||
|
account_id = int(state)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if not account_id:
|
||||||
|
return "Hata: State parametresi (account_id) bulunamadı."
|
||||||
|
|
||||||
|
account = request.env['mymach.social.account'].browse(account_id)
|
||||||
|
if not account.exists():
|
||||||
|
return request.render('http_routing.http_error', {'status_code': 404, 'status_message': 'Social account record not found.'})
|
||||||
|
|
||||||
|
# Process the code based on the platform
|
||||||
|
try:
|
||||||
|
account.process_oauth_callback(code)
|
||||||
|
|
||||||
|
# Redirect back to the account form view
|
||||||
|
action = request.env.ref('mymach_social_nextgen.action_social_account')
|
||||||
|
url = f"/web#id={account.id}&model=mymach.social.account&view_type=form&action={action.id}"
|
||||||
|
return werkzeug.utils.redirect(url)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
return request.render('http_routing.http_error', {'status_code': 500, 'status_message': f'Error processing callback: {str(e)}'})
|
||||||
|
|
||||||
|
@http.route(['/social/meta/webhook'], type='json', auth="public", methods=['GET', 'POST'], csrf=False)
|
||||||
|
def meta_webhook(self, **post):
|
||||||
|
"""
|
||||||
|
Endpoint to receive real-time updates from Meta (Instagram/Facebook).
|
||||||
|
Supports GET for verification, POST for receiving data.
|
||||||
|
"""
|
||||||
|
if request.httprequest.method == 'GET':
|
||||||
|
# Meta verification logic
|
||||||
|
verify_token = request.env['ir.config_parameter'].sudo().get_param('mymach_social.meta_verify_token')
|
||||||
|
mode = request.params.get('hub.mode')
|
||||||
|
token = request.params.get('hub.verify_token')
|
||||||
|
challenge = request.params.get('hub.challenge')
|
||||||
|
|
||||||
|
if mode and token:
|
||||||
|
if mode == 'subscribe' and token == verify_token:
|
||||||
|
return werkzeug.wrappers.Response(challenge, status=200)
|
||||||
|
else:
|
||||||
|
return werkzeug.wrappers.Response('Forbidden', status=403)
|
||||||
|
return werkzeug.wrappers.Response('Invalid Request', status=400)
|
||||||
|
|
||||||
|
elif request.httprequest.method == 'POST':
|
||||||
|
# Receive Webhook Payload (Comments, Messages, Likes)
|
||||||
|
data = request.jsonrequest
|
||||||
|
# Normalde burada data parse edilir ve mymach.social.stream.post kayıtları oluşturulur.
|
||||||
|
# Şimdilik canlı olmadığı için logluyoruz.
|
||||||
|
# _logger.info("Meta Webhook Received: %s", data)
|
||||||
|
return {'status': 'success'}
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
from odoo import http
|
||||||
|
from odoo.http import request
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class MetaWebhookController(http.Controller):
|
||||||
|
|
||||||
|
@http.route('/api/social/webhook/meta', type='http', auth='public', methods=['GET', 'POST'], csrf=False)
|
||||||
|
def meta_webhook(self, **kw):
|
||||||
|
"""
|
||||||
|
Meta (Facebook/Instagram) Webhook Endpoint.
|
||||||
|
GET isteği webhook verification (doğrulama) içindir.
|
||||||
|
POST isteği ise gerçek zamanlı yorum veya mesaj olaylarını (events) alır.
|
||||||
|
"""
|
||||||
|
if request.httprequest.method == 'GET':
|
||||||
|
# Verification for Meta Webhook setup
|
||||||
|
verify_token = request.env['ir.config_parameter'].sudo().get_param('mymach_social.meta_webhook_token', 'default_mymach_token')
|
||||||
|
mode = kw.get('hub.mode')
|
||||||
|
token = kw.get('hub.verify_token')
|
||||||
|
challenge = kw.get('hub.challenge')
|
||||||
|
|
||||||
|
if mode == 'subscribe' and token == verify_token:
|
||||||
|
_logger.info("Meta Webhook başarıyla doğrulandı.")
|
||||||
|
return request.make_response(challenge, headers=[('Content-Type', 'text/plain')])
|
||||||
|
else:
|
||||||
|
_logger.warning("Meta Webhook doğrulama hatası!")
|
||||||
|
return request.make_response('Verification failed', status=403)
|
||||||
|
|
||||||
|
elif request.httprequest.method == 'POST':
|
||||||
|
# Handle incoming webhook event
|
||||||
|
try:
|
||||||
|
data = json.loads(request.httprequest.data)
|
||||||
|
_logger.info(f"Meta Webhook veri alındı: {data}")
|
||||||
|
|
||||||
|
if data.get('object') == 'page':
|
||||||
|
for entry in data.get('entry', []):
|
||||||
|
page_id = entry.get('id')
|
||||||
|
# Hesabı (Sayfayı) bul
|
||||||
|
account = request.env['mymach.social.account'].sudo().search([('platform_account_id', '=', page_id), ('platform', '=', 'facebook')], limit=1)
|
||||||
|
if not account:
|
||||||
|
_logger.warning(f"Hesap bulunamadı (Page ID: {page_id})")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# İlgili stream'i bul (Feed stream)
|
||||||
|
stream = request.env['mymach.social.stream'].sudo().search([('account_id', '=', account.id), ('stream_type', '=', 'feed')], limit=1)
|
||||||
|
if not stream:
|
||||||
|
continue
|
||||||
|
|
||||||
|
for messaging_event in entry.get('changes', []):
|
||||||
|
value = messaging_event.get('value', {})
|
||||||
|
item = value.get('item')
|
||||||
|
|
||||||
|
# Sadece yorumları işle (basit örnek)
|
||||||
|
if item == 'comment' and value.get('verb') == 'add':
|
||||||
|
message_text = value.get('message')
|
||||||
|
author_name = value.get('from', {}).get('name', 'Bilinmeyen Kullanıcı')
|
||||||
|
post_id = value.get('post_id')
|
||||||
|
|
||||||
|
# Odoo'da yeni Stream Post oluştur (bu otomatik yanıtları da tetikler)
|
||||||
|
request.env['mymach.social.stream.post'].sudo().create({
|
||||||
|
'stream_id': stream.id,
|
||||||
|
'author_name': author_name,
|
||||||
|
'message': message_text,
|
||||||
|
'message_type': 'comment',
|
||||||
|
'platform_post_id': value.get('comment_id')
|
||||||
|
})
|
||||||
|
|
||||||
|
return request.make_response('EVENT_RECEIVED', status=200)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Meta Webhook işlenirken hata oluştu: {str(e)}")
|
||||||
|
return request.make_response('Error processing webhook', status=500)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<record id="ir_cron_scrape_competitors" model="ir.cron">
|
||||||
|
<field name="name">Sosyal Medya: Rakip Analizi Veri Kazıma</field>
|
||||||
|
<field name="model_id" ref="model_social_competitor"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model._cron_scrape_competitors()</field>
|
||||||
|
<field name="interval_number">1</field>
|
||||||
|
<field name="interval_type">days</field>
|
||||||
|
<field name="active" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
15
extra-addons/mymach_social_nextgen/data/social_post_cron.xml
Normal file
15
extra-addons/mymach_social_nextgen/data/social_post_cron.xml
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Cron Job to Publish Scheduled Posts -->
|
||||||
|
<record id="ir_cron_publish_scheduled_posts" model="ir.cron">
|
||||||
|
<field name="name">Social Media: Publish Scheduled Posts</field>
|
||||||
|
<field name="model_id" ref="model_mymach_social_post"/>
|
||||||
|
<field name="state">code</field>
|
||||||
|
<field name="code">model._cron_publish_scheduled_posts()</field>
|
||||||
|
<field name="interval_number">15</field>
|
||||||
|
<field name="interval_type">minutes</field>
|
||||||
|
<field name="active" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
517
extra-addons/mymach_social_nextgen/i18n/tr.po
Normal file
517
extra-addons/mymach_social_nextgen/i18n/tr.po
Normal file
@@ -0,0 +1,517 @@
|
|||||||
|
# Translation of Odoo Server.
|
||||||
|
# This file contains the translation of the following modules:
|
||||||
|
# * mymach_social_nextgen
|
||||||
|
#
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Project-Id-Version: Odoo Server 19.0-20260723\n"
|
||||||
|
"Report-Msgid-Bugs-To: \n"
|
||||||
|
"POT-Creation-Date: 2026-07-27 10:14+0000\n"
|
||||||
|
"PO-Revision-Date: 2026-07-27 10:14+0000\n"
|
||||||
|
"Last-Translator: \n"
|
||||||
|
"Language-Team: \n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: \n"
|
||||||
|
"Plural-Forms: \n"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__is_ai_generated
|
||||||
|
msgid "AI Generated"
|
||||||
|
msgstr "Yapay Zeka ile Üretildi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_form
|
||||||
|
msgid "API Settings"
|
||||||
|
msgstr "API Ayarları"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__access_token
|
||||||
|
msgid "Access Token"
|
||||||
|
msgstr "Erişim Jetonu"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__name
|
||||||
|
msgid "Account Name"
|
||||||
|
msgstr "Hesap Adı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_needaction
|
||||||
|
msgid "Action Needed"
|
||||||
|
msgstr "Eylem Gerekiyor"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__active
|
||||||
|
msgid "Active"
|
||||||
|
msgstr "Aktif"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_ids
|
||||||
|
msgid "Activities"
|
||||||
|
msgstr "Aktiviteler"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_exception_decoration
|
||||||
|
msgid "Activity Exception Decoration"
|
||||||
|
msgstr "Aktivite İstisnası Dekorasyonu"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_state
|
||||||
|
msgid "Activity State"
|
||||||
|
msgstr "Aktivite Durumu"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_type_icon
|
||||||
|
msgid "Activity Type Icon"
|
||||||
|
msgstr "Aktivite Türü İkonu"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__notes
|
||||||
|
msgid "Analysis Notes"
|
||||||
|
msgstr "Analiz Notları"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_analytics
|
||||||
|
msgid "Analytics"
|
||||||
|
msgstr "Analiz"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_attachment_count
|
||||||
|
msgid "Attachment Count"
|
||||||
|
msgstr "Ek Sayısı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__name
|
||||||
|
msgid "Competitor Name"
|
||||||
|
msgstr "Rakip Adı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_configuration
|
||||||
|
msgid "Configuration"
|
||||||
|
msgstr "Yapılandırma"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__content
|
||||||
|
msgid "Content"
|
||||||
|
msgstr "İçerik"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model_terms:ir.actions.act_window,help:mymach_social_nextgen.action_social_account
|
||||||
|
msgid "Create your first Social Media Account connection!"
|
||||||
|
msgstr "İlk Sosyal Medya Hesabı bağlantınızı oluşturun!"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__create_uid
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__create_uid
|
||||||
|
msgid "Created by"
|
||||||
|
msgstr "Oluşturan"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__create_date
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__create_date
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__create_date
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__create_date
|
||||||
|
msgid "Created on"
|
||||||
|
msgstr "Oluşturulma Tarihi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_dashboard
|
||||||
|
msgid "Dashboard"
|
||||||
|
msgstr "Kontrol Paneli"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__display_name
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__display_name
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__display_name
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__display_name
|
||||||
|
msgid "Display Name"
|
||||||
|
msgstr "Görünen Ad"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__draft
|
||||||
|
msgid "Draft"
|
||||||
|
msgstr "Taslak"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_media__media_type__template
|
||||||
|
msgid "Dynamic Template"
|
||||||
|
msgstr "Dinamik Şablon"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__engagement_rate
|
||||||
|
msgid "Engagement Rate (%)"
|
||||||
|
msgstr "Etkileşim Oranı (%)"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__error_message
|
||||||
|
msgid "Error Message"
|
||||||
|
msgstr "Hata Mesajı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__facebook
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__facebook
|
||||||
|
msgid "Facebook"
|
||||||
|
msgstr "Facebook"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__failed
|
||||||
|
msgid "Failed"
|
||||||
|
msgstr "Başarısız"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__file_data
|
||||||
|
msgid "File"
|
||||||
|
msgstr "Dosya"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__name
|
||||||
|
msgid "File Name"
|
||||||
|
msgstr "Dosya Adı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_follower_ids
|
||||||
|
msgid "Followers"
|
||||||
|
msgstr "Takipçiler"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_partner_ids
|
||||||
|
msgid "Followers (Partners)"
|
||||||
|
msgstr "Takipçiler (İş Ortakları)"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__followers_count
|
||||||
|
msgid "Followers Count"
|
||||||
|
msgstr "Takipçi Sayısı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_type_icon
|
||||||
|
msgid "Font awesome icon e.g. fa-tasks"
|
||||||
|
msgstr "Font awesome ikonu örn. fa-tasks"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__has_message
|
||||||
|
msgid "Has Message"
|
||||||
|
msgstr "Mesaj Var"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__id
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__id
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__id
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__id
|
||||||
|
msgid "ID"
|
||||||
|
msgstr "ID"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_exception_icon
|
||||||
|
msgid "Icon"
|
||||||
|
msgstr "İkon"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_exception_icon
|
||||||
|
msgid "Icon to indicate an exception activity."
|
||||||
|
msgstr "Bir istisna aktivitesini gösteren ikon."
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_needaction
|
||||||
|
msgid "If checked, new messages require your attention."
|
||||||
|
msgstr "İşaretliyse, yeni mesajlar dikkatinizi gerektirir."
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_has_error
|
||||||
|
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_has_sms_error
|
||||||
|
msgid "If checked, some messages have a delivery error."
|
||||||
|
msgstr "İşaretliyse, bazı mesajlarda teslimat hatası var."
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_media__media_type__image
|
||||||
|
msgid "Image"
|
||||||
|
msgstr "Görsel"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__instagram
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__instagram
|
||||||
|
msgid "Instagram"
|
||||||
|
msgstr "Instagram"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_is_follower
|
||||||
|
msgid "Is Follower"
|
||||||
|
msgstr "Takipçi mi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__last_scraped
|
||||||
|
msgid "Last Scraped Date"
|
||||||
|
msgstr "Son Taranma Tarihi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__write_uid
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__write_uid
|
||||||
|
msgid "Last Updated by"
|
||||||
|
msgstr "Son Güncelleyen"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__write_date
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__write_date
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__write_date
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__write_date
|
||||||
|
msgid "Last Updated on"
|
||||||
|
msgstr "Son Güncellenme Tarihi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__linkedin
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__linkedin
|
||||||
|
msgid "LinkedIn"
|
||||||
|
msgstr "LinkedIn"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__media_ids
|
||||||
|
msgid "Media Files"
|
||||||
|
msgstr "Medya Dosyaları"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_media
|
||||||
|
msgid "Media Library"
|
||||||
|
msgstr "Medya Kütüphanesi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__media_type
|
||||||
|
msgid "Media Type"
|
||||||
|
msgstr "Medya Türü"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_has_error
|
||||||
|
msgid "Message Delivery error"
|
||||||
|
msgstr "Mesaj Teslimat hatası"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_ids
|
||||||
|
msgid "Messages"
|
||||||
|
msgstr "Mesajlar"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__mimetype
|
||||||
|
msgid "Mime Type"
|
||||||
|
msgstr "Mime Türü"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__my_activity_date_deadline
|
||||||
|
msgid "My Activity Deadline"
|
||||||
|
msgstr "Aktivite Son Tarihim"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_root
|
||||||
|
msgid "MyMach Social"
|
||||||
|
msgstr "MyMach Social"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_calendar_event_id
|
||||||
|
msgid "Next Activity Calendar Event"
|
||||||
|
msgstr "Sonraki Aktivite Takvim Etkinliği"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_date_deadline
|
||||||
|
msgid "Next Activity Deadline"
|
||||||
|
msgstr "Sonraki Aktivite Son Tarihi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_summary
|
||||||
|
msgid "Next Activity Summary"
|
||||||
|
msgstr "Sonraki Aktivite Özeti"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_type_id
|
||||||
|
msgid "Next Activity Type"
|
||||||
|
msgstr "Sonraki Aktivite Türü"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_needaction_counter
|
||||||
|
msgid "Number of Actions"
|
||||||
|
msgstr "Eylem Sayısı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_has_error_counter
|
||||||
|
msgid "Number of errors"
|
||||||
|
msgstr "Hata sayısı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_needaction_counter
|
||||||
|
msgid "Number of messages requiring action"
|
||||||
|
msgstr "Eylem gerektiren mesaj sayısı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__message_has_error_counter
|
||||||
|
msgid "Number of messages with delivery error"
|
||||||
|
msgstr "Teslimat hatası olan mesaj sayısı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__platform
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__platform
|
||||||
|
msgid "Platform"
|
||||||
|
msgstr "Platform"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__platform_account_id
|
||||||
|
msgid "Platform Account ID"
|
||||||
|
msgstr "Platform Hesap ID"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__name
|
||||||
|
msgid "Post Title"
|
||||||
|
msgstr "Gönderi Başlığı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_posts
|
||||||
|
msgid "Posts"
|
||||||
|
msgstr "Gönderiler"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__profile_image_url
|
||||||
|
msgid "Profile Image URL"
|
||||||
|
msgstr "Profil Resmi URL"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_competitor__profile_url
|
||||||
|
msgid "Profile URL"
|
||||||
|
msgstr "Profil URL"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__published
|
||||||
|
msgid "Published"
|
||||||
|
msgstr "Yayınlandı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__published_date
|
||||||
|
msgid "Published Date"
|
||||||
|
msgstr "Yayınlanma Tarihi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__refresh_token
|
||||||
|
msgid "Refresh Token"
|
||||||
|
msgstr "Yenileme Jetonu"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__activity_user_id
|
||||||
|
msgid "Responsible User"
|
||||||
|
msgstr "Sorumlu Kullanıcı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__message_has_sms_error
|
||||||
|
msgid "SMS Delivery error"
|
||||||
|
msgstr "SMS Teslimat hatası"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__scheduled
|
||||||
|
msgid "Scheduled"
|
||||||
|
msgstr "Planlandı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__scheduled_date
|
||||||
|
msgid "Scheduled Date"
|
||||||
|
msgstr "Planlanma Tarihi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_form
|
||||||
|
msgid "Social Account"
|
||||||
|
msgstr "Sosyal Hesap"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.actions.act_window,name:mymach_social_nextgen.action_social_account
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__account_ids
|
||||||
|
#: model:ir.ui.menu,name:mymach_social_nextgen.menu_social_accounts
|
||||||
|
#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_tree
|
||||||
|
msgid "Social Accounts"
|
||||||
|
msgstr "Sosyal Hesaplar"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model,name:mymach_social_nextgen.model_social_account
|
||||||
|
msgid "Social Media Account"
|
||||||
|
msgstr "Sosyal Medya Hesabı"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model,name:mymach_social_nextgen.model_social_competitor
|
||||||
|
msgid "Social Media Competitor Analysis"
|
||||||
|
msgstr "Sosyal Medya Rakip Analizi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model,name:mymach_social_nextgen.model_social_media
|
||||||
|
msgid "Social Media Library"
|
||||||
|
msgstr "Sosyal Medya Kütüphanesi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model,name:mymach_social_nextgen.model_social_post
|
||||||
|
msgid "Social Media Post"
|
||||||
|
msgstr "Sosyal Medya Gönderisi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_media__template_id
|
||||||
|
msgid "Source Template"
|
||||||
|
msgstr "Kaynak Şablon"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__state
|
||||||
|
msgid "Status"
|
||||||
|
msgstr "Durum"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_state
|
||||||
|
msgid ""
|
||||||
|
"Status based on activities\n"
|
||||||
|
"Overdue: Due date is already passed\n"
|
||||||
|
"Today: Activity date is today\n"
|
||||||
|
"Planned: Future activities."
|
||||||
|
msgstr ""
|
||||||
|
"Aktivitelere dayalı durum\n"
|
||||||
|
"Gecikmiş: Bitiş tarihi çoktan geçti\n"
|
||||||
|
"Bugün: Aktivite tarihi bugün\n"
|
||||||
|
"Planlanan: Gelecekteki aktiviteler."
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_account__token_expiration
|
||||||
|
msgid "Token Expiration Date"
|
||||||
|
msgstr "Jeton Bitiş Tarihi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__activity_exception_decoration
|
||||||
|
msgid "Type of the exception activity on record."
|
||||||
|
msgstr "Kayıttaki istisna aktivitesinin türü."
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_media__media_type__video
|
||||||
|
msgid "Video"
|
||||||
|
msgstr "Video"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_post__state__approval
|
||||||
|
msgid "Waiting for Approval"
|
||||||
|
msgstr "Onay Bekliyor"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,field_description:mymach_social_nextgen.field_social_post__website_message_ids
|
||||||
|
msgid "Website Messages"
|
||||||
|
msgstr "Web Sitesi Mesajları"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields,help:mymach_social_nextgen.field_social_post__website_message_ids
|
||||||
|
msgid "Website communication history"
|
||||||
|
msgstr "Web sitesi iletişim geçmişi"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__x
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__x
|
||||||
|
msgid "X (Twitter)"
|
||||||
|
msgstr "X (Twitter)"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_account__platform__youtube
|
||||||
|
#: model:ir.model.fields.selection,name:mymach_social_nextgen.selection__social_competitor__platform__youtube
|
||||||
|
msgid "YouTube"
|
||||||
|
msgstr "YouTube"
|
||||||
|
|
||||||
|
#. module: mymach_social_nextgen
|
||||||
|
#: model_terms:ir.ui.view,arch_db:mymach_social_nextgen.view_social_account_form
|
||||||
|
msgid "e.g. MyMach Official Facebook"
|
||||||
|
msgstr "örn. MyMach Resmi Facebook"
|
||||||
113
extra-addons/mymach_social_nextgen/i18n/translate.py
Normal file
113
extra-addons/mymach_social_nextgen/i18n/translate.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
translations = {
|
||||||
|
"AI Generated": "Yapay Zeka ile Üretildi",
|
||||||
|
"API Settings": "API Ayarları",
|
||||||
|
"Access Token": "Erişim Jetonu",
|
||||||
|
"Account Name": "Hesap Adı",
|
||||||
|
"Action Needed": "Eylem Gerekiyor",
|
||||||
|
"Active": "Aktif",
|
||||||
|
"Activities": "Aktiviteler",
|
||||||
|
"Activity Exception Decoration": "Aktivite İstisnası Dekorasyonu",
|
||||||
|
"Activity State": "Aktivite Durumu",
|
||||||
|
"Activity Type Icon": "Aktivite Türü İkonu",
|
||||||
|
"Analysis Notes": "Analiz Notları",
|
||||||
|
"Analytics": "Analiz",
|
||||||
|
"Attachment Count": "Ek Sayısı",
|
||||||
|
"Competitor Name": "Rakip Adı",
|
||||||
|
"Configuration": "Yapılandırma",
|
||||||
|
"Content": "İçerik",
|
||||||
|
"Create your first Social Media Account connection!": "İlk Sosyal Medya Hesabı bağlantınızı oluşturun!",
|
||||||
|
"Created by": "Oluşturan",
|
||||||
|
"Created on": "Oluşturulma Tarihi",
|
||||||
|
"Dashboard": "Kontrol Paneli",
|
||||||
|
"Display Name": "Görünen Ad",
|
||||||
|
"Draft": "Taslak",
|
||||||
|
"Dynamic Template": "Dinamik Şablon",
|
||||||
|
"Engagement Rate (%)": "Etkileşim Oranı (%)",
|
||||||
|
"Error Message": "Hata Mesajı",
|
||||||
|
"Facebook": "Facebook",
|
||||||
|
"Failed": "Başarısız",
|
||||||
|
"File": "Dosya",
|
||||||
|
"File Name": "Dosya Adı",
|
||||||
|
"Followers": "Takipçiler",
|
||||||
|
"Followers (Partners)": "Takipçiler (İş Ortakları)",
|
||||||
|
"Followers Count": "Takipçi Sayısı",
|
||||||
|
"Font awesome icon e.g. fa-tasks": "Font awesome ikonu örn. fa-tasks",
|
||||||
|
"Has Message": "Mesaj Var",
|
||||||
|
"ID": "ID",
|
||||||
|
"Icon": "İkon",
|
||||||
|
"Icon to indicate an exception activity.": "Bir istisna aktivitesini gösteren ikon.",
|
||||||
|
"If checked, new messages require your attention.": "İşaretliyse, yeni mesajlar dikkatinizi gerektirir.",
|
||||||
|
"If checked, some messages have a delivery error.": "İşaretliyse, bazı mesajlarda teslimat hatası var.",
|
||||||
|
"Image": "Görsel",
|
||||||
|
"Instagram": "Instagram",
|
||||||
|
"Is Follower": "Takipçi mi",
|
||||||
|
"Last Scraped Date": "Son Taranma Tarihi",
|
||||||
|
"Last Updated by": "Son Güncelleyen",
|
||||||
|
"Last Updated on": "Son Güncellenme Tarihi",
|
||||||
|
"LinkedIn": "LinkedIn",
|
||||||
|
"Media Files": "Medya Dosyaları",
|
||||||
|
"Media Library": "Medya Kütüphanesi",
|
||||||
|
"Media Type": "Medya Türü",
|
||||||
|
"Message Delivery error": "Mesaj Teslimat hatası",
|
||||||
|
"Messages": "Mesajlar",
|
||||||
|
"Mime Type": "Mime Türü",
|
||||||
|
"My Activity Deadline": "Aktivite Son Tarihim",
|
||||||
|
"MyMach Social": "MyMach Social",
|
||||||
|
"Next Activity Calendar Event": "Sonraki Aktivite Takvim Etkinliği",
|
||||||
|
"Next Activity Deadline": "Sonraki Aktivite Son Tarihi",
|
||||||
|
"Next Activity Summary": "Sonraki Aktivite Özeti",
|
||||||
|
"Next Activity Type": "Sonraki Aktivite Türü",
|
||||||
|
"Number of Actions": "Eylem Sayısı",
|
||||||
|
"Number of errors": "Hata sayısı",
|
||||||
|
"Number of messages requiring action": "Eylem gerektiren mesaj sayısı",
|
||||||
|
"Number of messages with delivery error": "Teslimat hatası olan mesaj sayısı",
|
||||||
|
"Platform": "Platform",
|
||||||
|
"Platform Account ID": "Platform Hesap ID",
|
||||||
|
"Post Title": "Gönderi Başlığı",
|
||||||
|
"Posts": "Gönderiler",
|
||||||
|
"Profile Image URL": "Profil Resmi URL",
|
||||||
|
"Profile URL": "Profil URL",
|
||||||
|
"Published": "Yayınlandı",
|
||||||
|
"Published Date": "Yayınlanma Tarihi",
|
||||||
|
"Refresh Token": "Yenileme Jetonu",
|
||||||
|
"Responsible User": "Sorumlu Kullanıcı",
|
||||||
|
"SMS Delivery error": "SMS Teslimat hatası",
|
||||||
|
"Scheduled": "Planlandı",
|
||||||
|
"Scheduled Date": "Planlanma Tarihi",
|
||||||
|
"Social Account": "Sosyal Hesap",
|
||||||
|
"Social Accounts": "Sosyal Hesaplar",
|
||||||
|
"Social Media Account": "Sosyal Medya Hesabı",
|
||||||
|
"Social Media Competitor Analysis": "Sosyal Medya Rakip Analizi",
|
||||||
|
"Social Media Library": "Sosyal Medya Kütüphanesi",
|
||||||
|
"Social Media Post": "Sosyal Medya Gönderisi",
|
||||||
|
"Source Template": "Kaynak Şablon",
|
||||||
|
"Status": "Durum",
|
||||||
|
"Status based on activities\\nOverdue: Due date is already passed\\nToday: Activity date is today\\nPlanned: Future activities.": "Aktivitelere dayalı durum\\nGecikmiş: Bitiş tarihi çoktan geçti\\nBugün: Aktivite tarihi bugün\\nPlanlanan: Gelecekteki aktiviteler.",
|
||||||
|
"Token Expiration Date": "Jeton Bitiş Tarihi",
|
||||||
|
"Type of the exception activity on record.": "Kayıttaki istisna aktivitesinin türü.",
|
||||||
|
"Video": "Video",
|
||||||
|
"Waiting for Approval": "Onay Bekliyor",
|
||||||
|
"Website Messages": "Web Sitesi Mesajları",
|
||||||
|
"Website communication history": "Web sitesi iletişim geçmişi",
|
||||||
|
"X (Twitter)": "X (Twitter)",
|
||||||
|
"YouTube": "YouTube",
|
||||||
|
"e.g. MyMach Official Facebook": "örn. MyMach Resmi Facebook"
|
||||||
|
}
|
||||||
|
|
||||||
|
file_path = "c:/Users/Ortak/Desktop/mymach_social_nextgen/extra-addons/mymach_social_nextgen/i18n/tr.po"
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
for eng, tr in translations.items():
|
||||||
|
# Replacing single-line msgid
|
||||||
|
pattern = r'msgid "' + re.escape(eng) + r'"\nmsgstr ""'
|
||||||
|
replacement = 'msgid "' + eng + '"\nmsgstr "' + tr + '"'
|
||||||
|
content = re.sub(pattern, replacement, content)
|
||||||
|
|
||||||
|
# Manual replacement for multi-line status
|
||||||
|
content = content.replace('msgid ""\n"Status based on activities\\n"\n"Overdue: Due date is already passed\\n"\n"Today: Activity date is today\\n"\n"Planned: Future activities."\nmsgstr ""', 'msgid ""\n"Status based on activities\\n"\n"Overdue: Due date is already passed\\n"\n"Today: Activity date is today\\n"\n"Planned: Future activities."\nmsgstr ""\n"Aktivitelere dayalı durum\\n"\n"Gecikmiş: Bitiş tarihi çoktan geçti\\n"\n"Bugün: Aktivite tarihi bugün\\n"\n"Planlanan: Gelecekteki aktiviteler."')
|
||||||
|
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
f.write(content)
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
from . import social_account
|
from . import social_account
|
||||||
from . import social_post
|
from . import social_post
|
||||||
from . import social_media
|
from . import social_live_post
|
||||||
|
from . import social_stream
|
||||||
|
from . import social_stream_post
|
||||||
from . import social_competitor
|
from . import social_competitor
|
||||||
|
from . import res_config_settings
|
||||||
|
from . import res_company
|
||||||
|
from . import social_media
|
||||||
|
from . import social_auto_reply
|
||||||
|
from . import social_auto_reply_log
|
||||||
13
extra-addons/mymach_social_nextgen/models/res_company.py
Normal file
13
extra-addons/mymach_social_nextgen/models/res_company.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class ResCompany(models.Model):
|
||||||
|
_inherit = 'res.company'
|
||||||
|
|
||||||
|
social_watermark_image = fields.Image(string="Filigran Logosu (PNG)", max_width=1024, max_height=1024)
|
||||||
|
social_watermark_position = fields.Selection([
|
||||||
|
('bottom_right', 'Sağ Alt'),
|
||||||
|
('bottom_left', 'Sol Alt'),
|
||||||
|
('top_right', 'Sağ Üst'),
|
||||||
|
('top_left', 'Sol Üst'),
|
||||||
|
('center', 'Merkez')
|
||||||
|
], string="Filigran Konumu", default='bottom_right')
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class ResConfigSettings(models.TransientModel):
|
||||||
|
_inherit = 'res.config.settings'
|
||||||
|
|
||||||
|
# Meta (Facebook/Instagram) API
|
||||||
|
social_meta_client_id = fields.Char(string="Meta Client ID", config_parameter="mymach_social.meta_client_id")
|
||||||
|
social_meta_client_secret = fields.Char(string="Meta Client Secret", config_parameter="mymach_social.meta_client_secret")
|
||||||
|
|
||||||
|
# LinkedIn API
|
||||||
|
social_linkedin_client_id = fields.Char(string="LinkedIn Client ID", config_parameter="mymach_social.linkedin_client_id")
|
||||||
|
social_linkedin_client_secret = fields.Char(string="LinkedIn Client Secret", config_parameter="mymach_social.linkedin_client_secret")
|
||||||
|
|
||||||
|
# X (Twitter) API
|
||||||
|
social_x_client_id = fields.Char(string="X (Twitter) Client ID", config_parameter="mymach_social.x_client_id")
|
||||||
|
social_x_client_secret = fields.Char(string="X (Twitter) Client Secret", config_parameter="mymach_social.x_client_secret")
|
||||||
|
|
||||||
|
# YouTube API
|
||||||
|
social_youtube_client_id = fields.Char(string="YouTube Client ID", config_parameter="mymach_social.youtube_client_id")
|
||||||
|
social_youtube_client_secret = fields.Char(string="YouTube Client Secret", config_parameter="mymach_social.youtube_client_secret")
|
||||||
|
|
||||||
|
# AI APIs
|
||||||
|
social_gemini_api_key = fields.Char(string="Google Gemini API Key", config_parameter="mymach_social.gemini_api_key")
|
||||||
|
social_openai_api_key = fields.Char(string="OpenAI API Key", config_parameter="mymach_social.openai_api_key")
|
||||||
|
social_claude_api_key = fields.Char(string="Claude API Key", config_parameter="mymach_social.claude_api_key")
|
||||||
|
social_grok_api_key = fields.Char(string="Grok API Key", config_parameter="mymach_social.grok_api_key")
|
||||||
|
# Watermark Settings
|
||||||
|
social_watermark_image = fields.Image(related='company_id.social_watermark_image', readonly=False)
|
||||||
|
social_watermark_position = fields.Selection(related='company_id.social_watermark_position', readonly=False)
|
||||||
@@ -1,31 +1,144 @@
|
|||||||
from odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
from werkzeug.urls import url_join
|
||||||
|
|
||||||
|
from ..api.meta_api import MetaAPI
|
||||||
|
from ..api.instagram_api import InstagramAPI
|
||||||
|
from ..api.linkedin_api import LinkedInAPI
|
||||||
|
from ..api.x_api import XAPI
|
||||||
|
from ..api.youtube_api import YouTubeAPI
|
||||||
|
from ..api.tiktok_api import TikTokAPI
|
||||||
|
|
||||||
class SocialAccount(models.Model):
|
class SocialAccount(models.Model):
|
||||||
_name = 'social.account'
|
_name = 'mymach.social.account'
|
||||||
_description = 'Social Media Account'
|
_description = 'Social Media Account'
|
||||||
|
|
||||||
name = fields.Char(string='Account Name', required=True)
|
name = fields.Char(string='Hesap Adı', required=True)
|
||||||
platform = fields.Selection([
|
platform = fields.Selection([
|
||||||
('facebook', 'Facebook'),
|
('facebook', 'Facebook'),
|
||||||
('instagram', 'Instagram'),
|
('instagram', 'Instagram'),
|
||||||
|
('twitter', 'X (Twitter)'),
|
||||||
('linkedin', 'LinkedIn'),
|
('linkedin', 'LinkedIn'),
|
||||||
('x', 'X (Twitter)'),
|
('youtube', 'YouTube'),
|
||||||
('youtube', 'YouTube')
|
('tiktok', 'TikTok'),
|
||||||
|
('web_push', 'Web Push Bildirimleri')
|
||||||
], string='Platform', required=True)
|
], string='Platform', required=True)
|
||||||
|
|
||||||
active = fields.Boolean(default=True)
|
active = fields.Boolean(string='Aktif', default=True)
|
||||||
access_token = fields.Char(string='Access Token', groups="base.group_system")
|
access_token = fields.Char(string='Erişim Anahtarı (Access Token)', groups="base.group_system")
|
||||||
refresh_token = fields.Char(string='Refresh Token', groups="base.group_system")
|
refresh_token = fields.Char(string='Yenileme Anahtarı (Refresh Token)', groups="base.group_system")
|
||||||
token_expiration = fields.Datetime(string='Token Expiration Date')
|
token_expiration = fields.Datetime(string='Token Geçerlilik Tarihi')
|
||||||
|
|
||||||
# Optional fields for user info
|
# Optional fields for user info
|
||||||
platform_account_id = fields.Char(string='Platform Account ID')
|
platform_account_id = fields.Char(string='Platform Hesap ID')
|
||||||
profile_image_url = fields.Char(string='Profile Image URL')
|
profile_image_url = fields.Char(string='Profil Resmi URL')
|
||||||
|
|
||||||
|
color = fields.Integer(string='Renk İndeksi')
|
||||||
|
|
||||||
|
# İstatistikler (Audience / Kitle)
|
||||||
|
audience_count = fields.Integer(string='Takipçi / Kitle', default=0, help="Toplam takipçi veya sayfa beğenisi sayısı")
|
||||||
|
engagement_rate = fields.Float(string='Etkileşim Oranı (%)', default=0.0)
|
||||||
|
|
||||||
|
def _get_api_client(self):
|
||||||
|
self.ensure_one()
|
||||||
|
config = self.env['ir.config_parameter'].sudo()
|
||||||
|
base_url = config.get_param('web.base.url')
|
||||||
|
redirect_uri = url_join(base_url, f'/api/social/callback/{self.platform}')
|
||||||
|
|
||||||
|
if self.platform == 'facebook':
|
||||||
|
client_id = config.get_param('mymach_social.meta_client_id')
|
||||||
|
client_secret = config.get_param('mymach_social.meta_client_secret')
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
raise UserError("Meta API Client ID or Secret is missing in Settings.")
|
||||||
|
return MetaAPI(client_id, client_secret, redirect_uri)
|
||||||
|
|
||||||
|
elif self.platform == 'instagram':
|
||||||
|
client_id = config.get_param('mymach_social.meta_client_id')
|
||||||
|
client_secret = config.get_param('mymach_social.meta_client_secret')
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
raise UserError("Instagram API Client ID or Secret is missing in Settings.")
|
||||||
|
return InstagramAPI(client_id, client_secret, redirect_uri)
|
||||||
|
|
||||||
|
elif self.platform == 'linkedin':
|
||||||
|
client_id = config.get_param('mymach_social.linkedin_client_id')
|
||||||
|
client_secret = config.get_param('mymach_social.linkedin_client_secret')
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
raise UserError("LinkedIn API Client ID or Secret is missing in Settings.")
|
||||||
|
return LinkedInAPI(client_id, client_secret, redirect_uri)
|
||||||
|
|
||||||
|
elif self.platform == 'x':
|
||||||
|
client_id = config.get_param('mymach_social.x_client_id')
|
||||||
|
client_secret = config.get_param('mymach_social.x_client_secret')
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
raise UserError("X (Twitter) API Client ID or Secret is missing in Settings.")
|
||||||
|
return XAPI(client_id, client_secret, redirect_uri)
|
||||||
|
|
||||||
|
elif self.platform == 'youtube':
|
||||||
|
client_id = config.get_param('mymach_social.youtube_client_id')
|
||||||
|
client_secret = config.get_param('mymach_social.youtube_client_secret')
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
raise UserError("YouTube API Client ID or Secret is missing in Settings.")
|
||||||
|
return YouTubeAPI(client_id, client_secret, redirect_uri)
|
||||||
|
|
||||||
|
elif self.platform == 'tiktok':
|
||||||
|
client_id = config.get_param('mymach_social.tiktok_client_id')
|
||||||
|
client_secret = config.get_param('mymach_social.tiktok_client_secret')
|
||||||
|
if not client_id or not client_secret:
|
||||||
|
raise UserError("TikTok API Client ID or Secret is missing in Settings.")
|
||||||
|
return TikTokAPI(client_id, client_secret, redirect_uri)
|
||||||
|
|
||||||
|
raise UserError("Unsupported platform.")
|
||||||
|
|
||||||
def action_authenticate(self):
|
def action_authenticate(self):
|
||||||
# Placeholder for OAuth2 authentication flow
|
self.ensure_one()
|
||||||
pass
|
api_client = self._get_api_client()
|
||||||
|
auth_url = api_client.get_auth_url(state=str(self.id))
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_url',
|
||||||
|
'url': auth_url,
|
||||||
|
'target': 'self'
|
||||||
|
}
|
||||||
|
|
||||||
|
def process_oauth_callback(self, code):
|
||||||
|
self.ensure_one()
|
||||||
|
api_client = self._get_api_client()
|
||||||
|
token_data = api_client.exchange_code_for_token(code)
|
||||||
|
|
||||||
|
# Save tokens
|
||||||
|
self.access_token = token_data.get('access_token')
|
||||||
|
if 'refresh_token' in token_data:
|
||||||
|
self.refresh_token = token_data.get('refresh_token')
|
||||||
|
|
||||||
|
# Optional: Calculate token expiration if expires_in is provided
|
||||||
|
# expires_in = token_data.get('expires_in')
|
||||||
|
|
||||||
|
# In a real app, we would also fetch user profile data here.
|
||||||
|
|
||||||
def action_refresh_token(self):
|
def action_refresh_token(self):
|
||||||
# Placeholder for refreshing the OAuth2 token
|
self.ensure_one()
|
||||||
|
if self.platform == 'facebook' and self.access_token:
|
||||||
|
try:
|
||||||
|
api_client = self._get_api_client()
|
||||||
|
resp = api_client.refresh_token(self.access_token)
|
||||||
|
new_token = resp.get('access_token')
|
||||||
|
if new_token:
|
||||||
|
self.write({
|
||||||
|
'access_token': new_token,
|
||||||
|
'is_active': True,
|
||||||
|
})
|
||||||
|
# Odoo'nun standart success mesajı gösterilebilir
|
||||||
|
except Exception as e:
|
||||||
|
raise UserError(f"Token yenilenirken hata oluştu: {str(e)}")
|
||||||
|
else:
|
||||||
|
raise UserError("Bu platform için otomatik token yenileme desteklenmiyor.")
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _cron_refresh_tokens(self):
|
||||||
|
# 60 gün dolmadan yenilenmesi gerekenleri bul (şimdilik tüm aktif facebook hesaplarını deneyelim)
|
||||||
|
accounts = self.search([('is_active', '=', True), ('platform', '=', 'facebook')])
|
||||||
|
for acc in accounts:
|
||||||
|
try:
|
||||||
|
acc.action_refresh_token()
|
||||||
|
except Exception as e:
|
||||||
|
# Log error and continue with others
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
class SocialAutoReplyRule(models.Model):
|
||||||
|
_name = 'mymach.social.auto.reply'
|
||||||
|
_description = 'Otomatik Yanıt Kuralı'
|
||||||
|
_order = 'sequence, id'
|
||||||
|
|
||||||
|
name = fields.Char(string='Kural Adı', required=True)
|
||||||
|
active = fields.Boolean(string='Aktif', default=True)
|
||||||
|
sequence = fields.Integer(string='Öncelik', default=10, help="Düşük sayı daha önce çalışır.")
|
||||||
|
|
||||||
|
keyword = fields.Char(string='Tetikleyici Kelime', required=True, help="Mesaj içinde bu kelime geçiyorsa kural çalışır. Küçük/büyük harf duyarlı değildir.")
|
||||||
|
reply_message = fields.Text(string='Otomatik Yanıt Metni', required=True)
|
||||||
|
|
||||||
|
apply_to_message_type = fields.Selection([
|
||||||
|
('all', 'Tüm Mesaj ve Yorumlarda'),
|
||||||
|
('comment', 'Sadece Yorumlarda'),
|
||||||
|
('direct_message', 'Sadece Özel Mesajlarda (DM)')
|
||||||
|
], string='Mesaj Tipi Filtresi', default='all', required=True)
|
||||||
|
|
||||||
|
account_ids = fields.Many2many(
|
||||||
|
'mymach.social.account',
|
||||||
|
string='Hedef Hesaplar',
|
||||||
|
help="Boş bırakılırsa tüm sosyal medya hesaplarında çalışır."
|
||||||
|
)
|
||||||
|
|
||||||
|
log_ids = fields.One2many('mymach.social.auto.reply.log', 'rule_id', string='İşlem Geçmişi')
|
||||||
|
log_count = fields.Integer(string='Log Sayısı', compute='_compute_log_count')
|
||||||
|
|
||||||
|
@api.depends('log_ids')
|
||||||
|
def _compute_log_count(self):
|
||||||
|
for rule in self:
|
||||||
|
rule.log_count = len(rule.log_ids)
|
||||||
|
|
||||||
|
def action_view_logs(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': 'İşlem Geçmişi',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'mymach.social.auto.reply.log',
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'domain': [('rule_id', '=', self.id)],
|
||||||
|
'context': {'default_rule_id': self.id},
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
from odoo import models, fields
|
||||||
|
|
||||||
|
class SocialAutoReplyLog(models.Model):
|
||||||
|
_name = 'mymach.social.auto.reply.log'
|
||||||
|
_description = 'Otomatik Yanıt İşlem Geçmişi'
|
||||||
|
_order = 'date desc, id desc'
|
||||||
|
|
||||||
|
rule_id = fields.Many2one('mymach.social.auto.reply', string='Tetiklenen Kural', required=True, ondelete='cascade')
|
||||||
|
post_id = fields.Many2one('mymach.social.stream.post', string='İlgili Mesaj/Yorum', ondelete='set null')
|
||||||
|
account_id = fields.Many2one('mymach.social.account', string='Sosyal Hesap')
|
||||||
|
|
||||||
|
author_name = fields.Char(string='Yazar / Kullanıcı')
|
||||||
|
trigger_keyword = fields.Char(string='Tetikleyici Kelime')
|
||||||
|
sent_message = fields.Text(string='Gönderilen Yanıt')
|
||||||
|
|
||||||
|
status = fields.Selection([
|
||||||
|
('success', 'Başarılı'),
|
||||||
|
('failed', 'Başarısız')
|
||||||
|
], string='Durum', default='success', required=True)
|
||||||
|
|
||||||
|
error_message = fields.Text(string='Hata Mesajı')
|
||||||
|
date = fields.Datetime(string='İşlem Tarihi', default=fields.Datetime.now, required=True)
|
||||||
@@ -22,5 +22,19 @@ class SocialCompetitor(models.Model):
|
|||||||
notes = fields.Text(string='Analysis Notes')
|
notes = fields.Text(string='Analysis Notes')
|
||||||
|
|
||||||
def action_scrape_data(self):
|
def action_scrape_data(self):
|
||||||
# Placeholder for BeautifulSoup / Scrapy scraping logic
|
import random
|
||||||
pass
|
for record in self:
|
||||||
|
# Mock scraping logic. In a real module, use requests + bs4 or official API
|
||||||
|
new_followers = record.followers_count + random.randint(10, 500) if record.followers_count else random.randint(5000, 100000)
|
||||||
|
new_engagement = round(random.uniform(1.5, 8.5), 2)
|
||||||
|
|
||||||
|
record.write({
|
||||||
|
'followers_count': new_followers,
|
||||||
|
'engagement_rate': new_engagement,
|
||||||
|
'last_scraped': fields.Datetime.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _cron_scrape_competitors(self):
|
||||||
|
competitors = self.search([])
|
||||||
|
competitors.action_scrape_data()
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
class SocialLivePost(models.Model):
|
||||||
|
_name = 'mymach.social.live.post'
|
||||||
|
_description = 'Social Live Post'
|
||||||
|
_rec_name = 'account_id'
|
||||||
|
|
||||||
|
post_id = fields.Many2one('mymach.social.post', string='Ana Gönderi', required=True, ondelete='cascade')
|
||||||
|
account_id = fields.Many2one('mymach.social.account', string='Hesap', required=True, ondelete='cascade')
|
||||||
|
state = fields.Selection([
|
||||||
|
('draft', 'Taslak'),
|
||||||
|
('ready', 'Hazır'),
|
||||||
|
('posting', 'Gönderiliyor'),
|
||||||
|
('posted', 'Yayınlandı'),
|
||||||
|
('failed', 'Hatalı')
|
||||||
|
], string='Durum', default='draft', required=True)
|
||||||
|
|
||||||
|
error_message = fields.Text('Hata Mesajı')
|
||||||
|
|
||||||
|
# Platform-specific IDs
|
||||||
|
platform_post_id = fields.Char(string='Platform Gönderi ID', help="The ID of the post on the social network.")
|
||||||
|
|
||||||
|
# Engagement stats for this specific account
|
||||||
|
likes_count = fields.Integer(string='Beğeniler', default=0)
|
||||||
|
comments_count = fields.Integer(string='Yorumlar', default=0)
|
||||||
|
shares_count = fields.Integer(string='Paylaşımlar', default=0)
|
||||||
|
clicks_count = fields.Integer(string='Tıklamalar', default=0)
|
||||||
@@ -1,27 +1,25 @@
|
|||||||
from odoo import models, fields, api
|
from odoo import models, fields
|
||||||
|
|
||||||
class SocialMedia(models.Model):
|
class SocialMedia(models.Model):
|
||||||
_name = 'social.media'
|
_name = 'mymach.social.media'
|
||||||
_description = 'Social Media Library'
|
_description = 'Social Media Asset Library'
|
||||||
|
_order = 'id desc'
|
||||||
|
|
||||||
name = fields.Char(string='File Name', required=True)
|
name = fields.Char(string='Medya Adı', required=True)
|
||||||
media_type = fields.Selection([
|
media_type = fields.Selection([
|
||||||
('image', 'Image'),
|
('image', 'Görsel'),
|
||||||
('video', 'Video'),
|
('video', 'Video (Reels/Shorts)'),
|
||||||
('template', 'Dynamic Template')
|
('document', 'Belge/Diğer')
|
||||||
], string='Media Type', required=True)
|
], string='Medya Tipi', default='image', required=True)
|
||||||
|
|
||||||
file_data = fields.Binary(string='File', required=True)
|
attachment_id = fields.Many2one('ir.attachment', string='Dosya', required=True, ondelete='cascade')
|
||||||
mimetype = fields.Char(string='Mime Type')
|
image_preview = fields.Binary(related='attachment_id.datas', string="Önizleme", readonly=True)
|
||||||
|
|
||||||
# AI/Dynamic generation specific fields
|
tag_ids = fields.Many2many('mymach.social.media.tag', string='Etiketler')
|
||||||
is_ai_generated = fields.Boolean(string='AI Generated', default=False)
|
|
||||||
template_id = fields.Many2one('social.media', string='Source Template', domain=[('media_type', '=', 'template')])
|
|
||||||
|
|
||||||
def action_generate_watermark(self):
|
class SocialMediaTag(models.Model):
|
||||||
# Placeholder for PIL watermark logic
|
_name = 'mymach.social.media.tag'
|
||||||
pass
|
_description = 'Media Tag'
|
||||||
|
|
||||||
def action_generate_video(self):
|
name = fields.Char(string='Etiket', required=True)
|
||||||
# Placeholder for MoviePy video generation
|
color = fields.Integer(string='Renk İndeksi')
|
||||||
pass
|
|
||||||
|
|||||||
@@ -1,42 +1,297 @@
|
|||||||
from odoo import models, fields, api
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
import datetime
|
||||||
|
import logging
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class SocialPost(models.Model):
|
class SocialPost(models.Model):
|
||||||
_name = 'social.post'
|
_name = 'mymach.social.post'
|
||||||
_description = 'Social Media Post'
|
_description = 'Social Media Post'
|
||||||
_inherit = ['mail.thread', 'mail.activity.mixin']
|
_inherit = ['mail.thread', 'mail.activity.mixin']
|
||||||
|
_order = 'scheduled_date desc, id desc'
|
||||||
|
|
||||||
name = fields.Char(string='Post Title', required=True, tracking=True)
|
name = fields.Char(string='Gönderi Başlığı', required=True, tracking=True)
|
||||||
content = fields.Text(string='Content', tracking=True)
|
content = fields.Text(string='İçerik', required=True, tracking=True)
|
||||||
|
|
||||||
|
account_ids = fields.Many2many(
|
||||||
|
'mymach.social.account',
|
||||||
|
string='Sosyal Medya Hesapları',
|
||||||
|
required=True
|
||||||
|
)
|
||||||
|
|
||||||
|
post_type = fields.Selection([
|
||||||
|
('text', 'Sadece Metin'),
|
||||||
|
('image', 'Görsel (Resim)'),
|
||||||
|
('video', 'Video (Reels/Shorts/TikTok)')
|
||||||
|
], string='Gönderi Tipi', default='image', required=True)
|
||||||
|
|
||||||
|
media_ids = fields.Many2many('ir.attachment', string='Medyalar (Görseller)')
|
||||||
|
utm_campaign_id = fields.Many2one('utm.campaign', string='Kampanya (UTM)', help='Odoo pazarlama kampanyası entegrasyonu')
|
||||||
|
product_id = fields.Many2one('product.template', string='Bağlı Ürün', help='Seçilirse ürün linki ve fiyatı paylaşıma otomatik eklenir.')
|
||||||
|
|
||||||
|
scheduled_date = fields.Datetime(string='Planlanan Tarih', tracking=True)
|
||||||
|
published_date = fields.Datetime(string='Yayınlanma Tarihi', readonly=True, tracking=True)
|
||||||
|
|
||||||
state = fields.Selection([
|
state = fields.Selection([
|
||||||
('draft', 'Draft'),
|
('draft', 'Taslak'),
|
||||||
('approval', 'Waiting for Approval'),
|
('waiting', 'Onay Bekliyor'),
|
||||||
('scheduled', 'Scheduled'),
|
('scheduled', 'Planlandı'),
|
||||||
('published', 'Published'),
|
('published', 'Yayınlandı'),
|
||||||
('failed', 'Failed')
|
('failed', 'Hatalı')
|
||||||
], string='Status', default='draft', tracking=True)
|
], string='Durum', default='draft', tracking=True)
|
||||||
|
|
||||||
scheduled_date = fields.Datetime(string='Scheduled Date', tracking=True)
|
error_message = fields.Text(string='Hata Mesajı', readonly=True)
|
||||||
published_date = fields.Datetime(string='Published Date', readonly=True)
|
color = fields.Integer(string='Renk İndeksi')
|
||||||
|
|
||||||
account_ids = fields.Many2many('social.account', string='Social Accounts', required=True)
|
# Analytics Metrics
|
||||||
media_ids = fields.Many2many('social.media', string='Media Files')
|
likes_count = fields.Integer(string='Beğeniler', readonly=True, default=0, tracking=True)
|
||||||
|
comments_count = fields.Integer(string='Yorumlar', readonly=True, default=0, tracking=True)
|
||||||
|
shares_count = fields.Integer(string='Paylaşımlar', readonly=True, default=0, tracking=True)
|
||||||
|
|
||||||
error_message = fields.Text(string='Error Message', readonly=True)
|
click_count = fields.Integer(string='Tıklamalar / Ziyaretçiler', compute='_compute_click_count')
|
||||||
|
|
||||||
def action_submit_for_approval(self):
|
def _compute_click_count(self):
|
||||||
for record in self:
|
for post in self:
|
||||||
record.state = 'approval'
|
if not post.utm_campaign_id:
|
||||||
|
post.click_count = 0
|
||||||
|
continue
|
||||||
|
# Gelişmiş link.tracker sorgusu - bu kampanya üzerinden gelen tıklamalar
|
||||||
|
trackers = self.env['link.tracker'].search([('campaign_id', '=', post.utm_campaign_id.id)])
|
||||||
|
post.click_count = sum(trackers.mapped('count'))
|
||||||
|
|
||||||
def action_approve(self):
|
live_post_ids = fields.One2many('mymach.social.live.post', 'post_id', string='Canlı Gönderiler', readonly=True)
|
||||||
for record in self:
|
|
||||||
if record.scheduled_date:
|
# 1. Gönderi Önizleme (Live Preview)
|
||||||
record.state = 'scheduled'
|
preview_html = fields.Html(string="Önizleme", compute="_compute_preview_html")
|
||||||
else:
|
|
||||||
record.action_publish()
|
# 5. Platform Validasyonu (Constraint)
|
||||||
|
@api.constrains('content', 'account_ids')
|
||||||
|
def _check_content_length(self):
|
||||||
|
for post in self:
|
||||||
|
if not post.content:
|
||||||
|
continue
|
||||||
|
for account in post.account_ids:
|
||||||
|
if account.platform == 'twitter' and len(post.content) > 280:
|
||||||
|
raise UserError(f"X (Twitter) için karakter sınırı 280'dir. Şu an {len(post.content)} karakter yazdınız.")
|
||||||
|
elif account.platform == 'linkedin' and len(post.content) > 3000:
|
||||||
|
raise UserError(f"LinkedIn için karakter sınırı 3000'dir. Şu an {len(post.content)} karakter yazdınız.")
|
||||||
|
|
||||||
|
@api.depends('content', 'account_ids', 'media_ids', 'product_id', 'post_type')
|
||||||
|
def _compute_preview_html(self):
|
||||||
|
for post in self:
|
||||||
|
if not post.content and not post.media_ids:
|
||||||
|
post.preview_html = "<div class='text-muted text-center p-5'>Gönderi önizlemesi için içerik veya medya ekleyin...</div>"
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Mobil Mock-up tasarımı (iPhone Tarzı)
|
||||||
|
preview_content = post.content or ""
|
||||||
|
if post.product_id:
|
||||||
|
price = post.product_id.list_price
|
||||||
|
link = f"https://mymach.com/shop/product/{post.product_id.id}"
|
||||||
|
preview_content += f"\n\nSatın Al: {post.product_id.name} - {price} ₺\n{link}"
|
||||||
|
|
||||||
|
media_label = "Medyalar Yüklenecek"
|
||||||
|
if post.post_type == 'video':
|
||||||
|
media_label = f"▶ Video Önizlemesi ({len(post.media_ids)} Dosya)"
|
||||||
|
elif post.post_type == 'image':
|
||||||
|
media_label = f"📷 Görsel Alanı ({len(post.media_ids)} Dosya)"
|
||||||
|
|
||||||
|
preview = f"""
|
||||||
|
<div style="margin: 0 auto; width: 320px; height: 600px; border: 12px solid #333; border-radius: 36px; position: relative; background: #fafafa; overflow: hidden; box-shadow: 0 10px 25px rgba(0,0,0,0.1);">
|
||||||
|
<!-- Notch -->
|
||||||
|
<div style="position: absolute; top: 0; left: 50%; transform: translateX(-50%); width: 120px; height: 25px; background: #333; border-bottom-left-radius: 15px; border-bottom-right-radius: 15px; z-index: 10;"></div>
|
||||||
|
|
||||||
|
<!-- App Header -->
|
||||||
|
<div style="padding: 35px 15px 10px; background: #fff; border-bottom: 1px solid #ddd; display: flex; align-items: center; justify-content: space-between;">
|
||||||
|
<div style="font-weight: bold; font-family: sans-serif; font-size: 18px;">SocialApp</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Post Area -->
|
||||||
|
<div style="background: #fff; margin-top: 10px; padding: 15px; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;">
|
||||||
|
<!-- User Info -->
|
||||||
|
<div style="display: flex; align-items: center; margin-bottom: 12px;">
|
||||||
|
<div style="width: 36px; height: 36px; border-radius: 50%; background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%); display: flex; align-items: center; justify-content: center; margin-right: 10px;">
|
||||||
|
<div style="width: 32px; height: 32px; border-radius: 50%; background: #fff; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 12px; color: #333;">MM</div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="font-weight: 600; font-size: 13px; color: #262626;">MyMach Official</div>
|
||||||
|
<div style="color: #8e8e8e; font-size: 11px;">Sponsored</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Media Placeholder -->
|
||||||
|
<div style="width: 100%; height: 250px; background: #efefef; border-radius: 4px; display: flex; align-items: center; justify-content: center; color: #8e8e8e; font-size: 14px; margin-bottom: 12px;">
|
||||||
|
{media_label}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<div style="display: flex; gap: 12px; margin-bottom: 10px; color: #262626; font-size: 18px;">
|
||||||
|
♡ 💬 ➦
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Content -->
|
||||||
|
<div style="font-size: 13px; color: #262626; white-space: pre-wrap; line-height: 1.4;">
|
||||||
|
<span style="font-weight: 600;">MyMach Official</span> {preview_content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
"""
|
||||||
|
post.preview_html = preview
|
||||||
|
|
||||||
|
# 4. Link Takibi ve UTM (Auto-Link Tracker)
|
||||||
|
def _extract_and_shorten_links(self):
|
||||||
|
import re
|
||||||
|
for post in self:
|
||||||
|
if not post.content:
|
||||||
|
continue
|
||||||
|
# Temel URL regex'i
|
||||||
|
urls = re.findall(r'(https?://[^\s]+)', post.content)
|
||||||
|
new_content = post.content
|
||||||
|
for url in urls:
|
||||||
|
# Odoo'nun link.tracker modelini kullanarak UTM'li kısa link yarat
|
||||||
|
tracker = self.env['link.tracker'].create({
|
||||||
|
'url': url,
|
||||||
|
'campaign_id': post.utm_campaign_id.id if post.utm_campaign_id else False,
|
||||||
|
'title': f"{post.name} Linki"
|
||||||
|
})
|
||||||
|
# Kısa linkle orijinali değiştir
|
||||||
|
new_content = new_content.replace(url, tracker.short_url)
|
||||||
|
|
||||||
|
if new_content != post.content:
|
||||||
|
post.content = new_content
|
||||||
|
|
||||||
|
def action_view_visitors(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': 'Bağlantı İzleyici (Ziyaretçiler)',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'link.tracker',
|
||||||
|
'view_mode': 'list,form',
|
||||||
|
'domain': [('campaign_id', '=', self.utm_campaign_id.id)]
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_open_ai_wizard(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': 'AI ile İçerik Üret',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'mymach.social.ai.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'default_post_id': self.id}
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_open_ai_image_wizard(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': 'AI ile Görsel Üret',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'mymach.social.ai.image.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'default_post_id': self.id}
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_request_approval(self):
|
||||||
|
self.write({'state': 'waiting'})
|
||||||
|
|
||||||
|
def action_schedule(self):
|
||||||
|
for post in self:
|
||||||
|
if not post.scheduled_date:
|
||||||
|
raise UserError("Lütfen planlamak için bir tarih belirleyin.")
|
||||||
|
if post.scheduled_date < fields.Datetime.now():
|
||||||
|
raise UserError("Planlanmış tarih geçmişte olamaz.")
|
||||||
|
self.write({'state': 'scheduled'})
|
||||||
|
|
||||||
|
def action_publish_now(self):
|
||||||
|
self.write({'scheduled_date': fields.Datetime.now()})
|
||||||
|
self.action_publish()
|
||||||
|
|
||||||
def action_publish(self):
|
def action_publish(self):
|
||||||
for record in self:
|
# Eğer ürün seçiliyse metne ekle
|
||||||
# Placeholder for publishing logic
|
for post in self:
|
||||||
record.state = 'published'
|
if post.product_id and post.content:
|
||||||
record.published_date = fields.Datetime.now()
|
price = post.product_id.list_price
|
||||||
|
|
||||||
|
# UTM Etiketleri Ekle (Analytics İzleme)
|
||||||
|
campaign_name = post.utm_campaign_id.name if post.utm_campaign_id else "social_post"
|
||||||
|
campaign_name = campaign_name.replace(' ', '_').lower()
|
||||||
|
link = f"https://mymach.com/shop/product/{post.product_id.id}?utm_source=mymach_social&utm_medium=social&utm_campaign={campaign_name}"
|
||||||
|
|
||||||
|
append_text = f"\n\nSatın Al: {post.product_id.name} - {price} ₺\n{link}"
|
||||||
|
if append_text not in post.content:
|
||||||
|
post.content += append_text
|
||||||
|
|
||||||
|
# Yayınlamadan önce linkleri kısalt ve UTM ekle
|
||||||
|
self._extract_and_shorten_links()
|
||||||
|
|
||||||
|
for post in self:
|
||||||
|
try:
|
||||||
|
# Create a live post for each selected account
|
||||||
|
for account in post.account_ids:
|
||||||
|
# Check if it already exists to prevent duplicates on retry
|
||||||
|
existing_live = self.env['mymach.social.live.post'].search([
|
||||||
|
('post_id', '=', post.id),
|
||||||
|
('account_id', '=', account.id)
|
||||||
|
], limit=1)
|
||||||
|
|
||||||
|
if not existing_live:
|
||||||
|
live_post = self.env['mymach.social.live.post'].create({
|
||||||
|
'post_id': post.id,
|
||||||
|
'account_id': account.id,
|
||||||
|
'state': 'ready'
|
||||||
|
})
|
||||||
|
api_client = None
|
||||||
|
try:
|
||||||
|
api_client = account._get_api_client()
|
||||||
|
except Exception as e:
|
||||||
|
_logger.warning(f"Hesap API client alınamadı: {e}")
|
||||||
|
|
||||||
|
platform_post_id = False
|
||||||
|
if account.platform == 'facebook' and account.access_token and api_client:
|
||||||
|
page_id = account.platform_account_id or 'me'
|
||||||
|
# API İsteği
|
||||||
|
resp = api_client.publish_post(page_id, account.access_token, post.content)
|
||||||
|
platform_post_id = resp.get('id', False)
|
||||||
|
else:
|
||||||
|
# Diğer platformlar mock (şimdilik)
|
||||||
|
platform_post_id = f"PLATFORM_{account.id}_{post.id}"
|
||||||
|
|
||||||
|
live_post.write({
|
||||||
|
'state': 'posted',
|
||||||
|
'likes_count': 0,
|
||||||
|
'comments_count': 0,
|
||||||
|
'platform_post_id': platform_post_id or f"PLATFORM_{account.id}_{post.id}"
|
||||||
|
})
|
||||||
|
|
||||||
|
# Overall post metrics can be aggregated or simulated
|
||||||
|
post.write({
|
||||||
|
'state': 'published',
|
||||||
|
'published_date': fields.Datetime.now(),
|
||||||
|
'error_message': False,
|
||||||
|
'likes_count': sum(post.live_post_ids.mapped('likes_count')),
|
||||||
|
'comments_count': sum(post.live_post_ids.mapped('comments_count')),
|
||||||
|
'shares_count': sum(post.live_post_ids.mapped('shares_count')),
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
post.write({
|
||||||
|
'state': 'failed',
|
||||||
|
'error_message': f"Yayınlama Hatası: {str(e)}"
|
||||||
|
})
|
||||||
|
|
||||||
|
def action_reset_to_draft(self):
|
||||||
|
self.write({'state': 'draft', 'error_message': False})
|
||||||
|
|
||||||
|
@api.model
|
||||||
|
def _cron_publish_scheduled_posts(self):
|
||||||
|
_logger.info("Cron: Zamanlanmış gönderiler kontrol ediliyor...")
|
||||||
|
posts_to_publish = self.search([
|
||||||
|
('state', '=', 'scheduled'),
|
||||||
|
('scheduled_date', '<=', fields.Datetime.now())
|
||||||
|
])
|
||||||
|
if posts_to_publish:
|
||||||
|
_logger.info(f"Cron: Yayınlanacak {len(posts_to_publish)} gönderi bulundu.")
|
||||||
|
posts_to_publish.action_publish()
|
||||||
|
|||||||
17
extra-addons/mymach_social_nextgen/models/social_stream.py
Normal file
17
extra-addons/mymach_social_nextgen/models/social_stream.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
class SocialStream(models.Model):
|
||||||
|
_name = 'mymach.social.stream'
|
||||||
|
_description = 'Social Media Stream'
|
||||||
|
|
||||||
|
name = fields.Char(string='Akış Adı', required=True)
|
||||||
|
account_id = fields.Many2one('mymach.social.account', string='Hesap', required=True, ondelete='cascade')
|
||||||
|
stream_post_ids = fields.One2many('mymach.social.stream.post', 'stream_id', string='Akış Gönderileri')
|
||||||
|
stream_type = fields.Selection([
|
||||||
|
('feed', 'Zaman Tüneli / Feed'),
|
||||||
|
('mentions', 'Bahsetmeler'),
|
||||||
|
('messages', 'Mesajlar'),
|
||||||
|
('keyword', 'Anahtar Kelime (Dinleme)')
|
||||||
|
], string='Akış Tipi', default='feed', required=True)
|
||||||
|
|
||||||
|
search_keyword = fields.Char(string='Takip Edilecek Kelime/Hashtag', help='Sadece Akış Tipi "Anahtar Kelime" ise kullanılır. Örn: #teknoloji')
|
||||||
140
extra-addons/mymach_social_nextgen/models/social_stream_post.py
Normal file
140
extra-addons/mymach_social_nextgen/models/social_stream_post.py
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
|
||||||
|
class SocialStreamPost(models.Model):
|
||||||
|
_name = 'mymach.social.stream.post'
|
||||||
|
_inherit = ['mail.thread']
|
||||||
|
_description = 'Social Stream Post'
|
||||||
|
_order = 'published_date desc'
|
||||||
|
|
||||||
|
stream_id = fields.Many2one('mymach.social.stream', string='Akış', required=True, ondelete='cascade')
|
||||||
|
account_id = fields.Many2one(related='stream_id.account_id', string='Hesap', store=True)
|
||||||
|
author_name = fields.Char(string='Yazar')
|
||||||
|
author_link = fields.Char(string='Yazar Profil Linki')
|
||||||
|
|
||||||
|
published_date = fields.Datetime(string='Yayın Tarihi')
|
||||||
|
message = fields.Text(string='Mesaj / İçerik')
|
||||||
|
link_url = fields.Char(string='Gönderi Linki')
|
||||||
|
|
||||||
|
likes_count = fields.Integer(string='Beğeniler')
|
||||||
|
comments_count = fields.Integer(string='Yorumlar')
|
||||||
|
shares_count = fields.Integer(string='Paylaşımlar')
|
||||||
|
|
||||||
|
platform_post_id = fields.Char(string='Platform ID')
|
||||||
|
|
||||||
|
sentiment = fields.Selection([
|
||||||
|
('positive', '😊 Olumlu'),
|
||||||
|
('neutral', '😐 Nötr'),
|
||||||
|
('negative', '😡 Olumsuz')
|
||||||
|
], string='Duygu Analizi', default='neutral')
|
||||||
|
|
||||||
|
message_type = fields.Selection([
|
||||||
|
('comment', 'Yorum'),
|
||||||
|
('direct_message', 'Özel Mesaj (DM)')
|
||||||
|
], string='Mesaj Tipi', default='comment', required=True)
|
||||||
|
|
||||||
|
@api.model_create_multi
|
||||||
|
def create(self, vals_list):
|
||||||
|
records = super().create(vals_list)
|
||||||
|
for record in records:
|
||||||
|
if not record.message:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Otomatik Yanıt Kurallarını Kontrol Et
|
||||||
|
rules = self.env['mymach.social.auto.reply'].search([('active', '=', True)], order='sequence, id')
|
||||||
|
for rule in rules:
|
||||||
|
# Hesap Filtresi Kontrolü
|
||||||
|
if rule.account_ids and record.account_id not in rule.account_ids:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Mesaj Tipi Filtresi Kontrolü
|
||||||
|
if rule.apply_to_message_type != 'all' and rule.apply_to_message_type != record.message_type:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Kelime Kontrolü
|
||||||
|
if rule.keyword.lower() in record.message.lower():
|
||||||
|
# Yanıtı gönder
|
||||||
|
record.message_post(body=f"🤖 <b>Otomatik Yanıt Gönderildi:</b><br/>'{rule.keyword}' kelimesi algılandı.<br/><b>Yanıt:</b> {rule.reply_message}")
|
||||||
|
|
||||||
|
# Log kaydını oluştur
|
||||||
|
self.env['mymach.social.auto.reply.log'].create({
|
||||||
|
'rule_id': rule.id,
|
||||||
|
'post_id': record.id,
|
||||||
|
'account_id': record.account_id.id,
|
||||||
|
'author_name': record.author_name,
|
||||||
|
'trigger_keyword': rule.keyword,
|
||||||
|
'sent_message': rule.reply_message,
|
||||||
|
'status': 'success'
|
||||||
|
})
|
||||||
|
break # İlk eşleşen kural çalışsın ve döngü bitsin.
|
||||||
|
|
||||||
|
return records
|
||||||
|
|
||||||
|
def action_analyze_sentiment(self):
|
||||||
|
# API anahtarını al
|
||||||
|
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
|
||||||
|
|
||||||
|
for post in self:
|
||||||
|
if not post.message:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if api_key:
|
||||||
|
# Gerçek AI Analizi (Gemini)
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
||||||
|
system_prompt = "Sen bir duygu analizi uzmanısın. Verilen metnin duygusunu analiz et ve sadece şu üç kelimeden birini döndür: 'positive', 'neutral', 'negative'."
|
||||||
|
payload = {
|
||||||
|
"system_instruction": {"parts": [{"text": system_prompt}]},
|
||||||
|
"contents": [{"parts": [{"text": f"Metin: {post.message}"}]}],
|
||||||
|
"generationConfig": {"temperature": 0.1, "maxOutputTokens": 10}
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload)
|
||||||
|
if response.status_code == 200:
|
||||||
|
result = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '').strip().lower()
|
||||||
|
if 'positive' in result:
|
||||||
|
post.sentiment = 'positive'
|
||||||
|
elif 'negative' in result:
|
||||||
|
post.sentiment = 'negative'
|
||||||
|
else:
|
||||||
|
post.sentiment = 'neutral'
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
# Log error, fallback to basic logic
|
||||||
|
pass
|
||||||
|
|
||||||
|
# Fallback (API Key yoksa veya hata olursa) Basit kelime analizi
|
||||||
|
lower_msg = post.message.lower()
|
||||||
|
if any(word in lower_msg for word in ['harika', 'süper', 'teşekkür', 'iyi', 'başarılı']):
|
||||||
|
post.sentiment = 'positive'
|
||||||
|
elif any(word in lower_msg for word in ['kötü', 'rezalet', 'şikayet', 'sorun', 'hata', 'berbat']):
|
||||||
|
post.sentiment = 'negative'
|
||||||
|
else:
|
||||||
|
post.sentiment = 'neutral'
|
||||||
|
|
||||||
|
def action_suggest_reply(self):
|
||||||
|
self.ensure_one()
|
||||||
|
return {
|
||||||
|
'name': 'AI ile Yanıt Öner',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'mymach.social.ai.reply.wizard',
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'new',
|
||||||
|
'context': {'default_stream_post_id': self.id}
|
||||||
|
}
|
||||||
|
|
||||||
|
def action_create_lead(self):
|
||||||
|
self.ensure_one()
|
||||||
|
# Odoo CRM'de yeni bir Lead (Fırsat/Müşteri Adayı) yarat
|
||||||
|
lead = self.env['crm.lead'].create({
|
||||||
|
'name': f"Sosyal Medya Fırsatı: {self.author_name}",
|
||||||
|
'description': f"İçerik: {self.message}\nLink: {self.link_url}",
|
||||||
|
'type': 'opportunity'
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
'name': 'Yeni Müşteri Adayı',
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'crm.lead',
|
||||||
|
'res_id': lead.id,
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'current'
|
||||||
|
}
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||||
access_social_account_user,social.account.user,model_social_account,base.group_user,1,1,1,0
|
access_mymach_social_account_user,mymach.social.account.user,model_mymach_social_account,base.group_user,1,1,1,1
|
||||||
access_social_account_manager,social.account.manager,model_social_account,base.group_system,1,1,1,1
|
access_mymach_social_post_user,mymach.social.post.user,model_mymach_social_post,base.group_user,1,1,1,1
|
||||||
access_social_post_user,social.post.user,model_social_post,base.group_user,1,1,1,0
|
access_mymach_social_ai_wizard_user,mymach.social.ai.wizard.user,model_mymach_social_ai_wizard,base.group_user,1,1,1,1
|
||||||
access_social_post_manager,social.post.manager,model_social_post,base.group_system,1,1,1,1
|
access_mymach_social_live_post_user,mymach.social.live.post.user,model_mymach_social_live_post,base.group_user,1,1,1,1
|
||||||
access_social_media_user,social.media.user,model_social_media,base.group_user,1,1,1,0
|
access_mymach_social_stream_user,mymach.social.stream.user,model_mymach_social_stream,base.group_user,1,1,1,1
|
||||||
access_social_media_manager,social.media.manager,model_social_media,base.group_system,1,1,1,1
|
access_mymach_social_stream_post_user,mymach.social.stream.post.user,model_mymach_social_stream_post,base.group_user,1,1,1,1
|
||||||
access_social_competitor_user,social.competitor.user,model_social_competitor,base.group_user,1,1,1,0
|
access_mymach_social_competitor_user,social.competitor.user,model_social_competitor,base.group_user,1,1,1,1
|
||||||
access_social_competitor_manager,social.competitor.manager,model_social_competitor,base.group_system,1,1,1,1
|
access_mymach_social_ai_image_wizard_user,mymach.social.ai.image.wizard.user,model_mymach_social_ai_image_wizard,base.group_user,1,1,1,1
|
||||||
|
access_mymach_social_ai_reply_wizard_user,mymach.social.ai.reply.wizard.user,model_mymach_social_ai_reply_wizard,base.group_user,1,1,1,1
|
||||||
|
access_social_media,access.mymach.social.media,model_mymach_social_media,base.group_user,1,1,1,1
|
||||||
|
access_social_media_tag,access.mymach.social.media.tag,model_mymach_social_media_tag,base.group_user,1,1,1,1
|
||||||
|
access_social_auto_reply,access.mymach.social.auto.reply,model_mymach_social_auto_reply,base.group_user,1,1,1,1
|
||||||
|
access_social_auto_reply_log,access.mymach.social.auto.reply.log,model_mymach_social_auto_reply_log,base.group_user,1,1,1,1
|
||||||
|
54
extra-addons/mymach_social_nextgen/security/ir_rule.xml
Normal file
54
extra-addons/mymach_social_nextgen/security/ir_rule.xml
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!--
|
||||||
|
Account Modeli İçin Kurallar
|
||||||
|
-->
|
||||||
|
<record id="rule_social_account_user" model="ir.rule">
|
||||||
|
<field name="name">Social Account: User Rule</field>
|
||||||
|
<field name="model_id" ref="model_mymach_social_account"/>
|
||||||
|
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_social_account_manager" model="ir.rule">
|
||||||
|
<field name="name">Social Account: Manager Rule</field>
|
||||||
|
<field name="model_id" ref="model_mymach_social_account"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Post Modeli İçin Kurallar
|
||||||
|
-->
|
||||||
|
<record id="rule_social_post_user" model="ir.rule">
|
||||||
|
<field name="name">Social Post: User Rule</field>
|
||||||
|
<field name="model_id" ref="model_mymach_social_post"/>
|
||||||
|
<field name="domain_force">[('create_uid', '=', user.id)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="True"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="rule_social_post_manager" model="ir.rule">
|
||||||
|
<field name="name">Social Post: Manager Rule</field>
|
||||||
|
<field name="model_id" ref="model_mymach_social_post"/>
|
||||||
|
<field name="domain_force">[(1, '=', 1)]</field>
|
||||||
|
<field name="groups" eval="[(4, ref('base.group_system'))]"/>
|
||||||
|
<field name="perm_read" eval="True"/>
|
||||||
|
<field name="perm_write" eval="True"/>
|
||||||
|
<field name="perm_create" eval="True"/>
|
||||||
|
<field name="perm_unlink" eval="True"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
19
extra-addons/mymach_social_nextgen/security/security.xml
Normal file
19
extra-addons/mymach_social_nextgen/security/security.xml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data noupdate="1">
|
||||||
|
<!-- Sosyal Medya Uzmanı (Normal Kullanıcı) -->
|
||||||
|
<record id="group_social_user" model="res.groups">
|
||||||
|
<field name="name">Sosyal Medya Uzmanı</field>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
|
||||||
|
<field name="comment">Gönderi taslağı hazırlayabilir ve onay bekler statüsüne alabilir.</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Sosyal Medya Yöneticisi (Manager) -->
|
||||||
|
<record id="group_social_manager" model="res.groups">
|
||||||
|
<field name="name">Sosyal Medya Yöneticisi</field>
|
||||||
|
<field name="implied_ids" eval="[(4, ref('group_social_user'))]"/>
|
||||||
|
<field name="comment">Taslakları onaylayabilir, doğrudan yayınlayabilir ve gelişmiş ayarları görebilir.</field>
|
||||||
|
<field name="user_ids" eval="[(4, ref('base.user_root')), (4, ref('base.user_admin'))]"/>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
/* mymach_social_nextgen/static/src/css/social_dashboard.css */
|
||||||
|
|
||||||
|
/* 1. KANBAN VIEW (ANA ARKA PLAN) */
|
||||||
|
.o_kanban_view.o_social_dashboard_kanban {
|
||||||
|
background-color: #020617 !important; /* Çok daha koyu bir arka plan (Slate-950) */
|
||||||
|
padding: 16px !important;
|
||||||
|
min-height: 100vh !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 2. SÜTUNLAR (STREAMS) */
|
||||||
|
.o_social_dashboard_kanban .o_kanban_group {
|
||||||
|
background-color: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
padding: 0 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sütun Başlığı (Header) */
|
||||||
|
.o_social_dashboard_kanban .o_kanban_header {
|
||||||
|
background-color: #334155 !important; /* Arka plana göre çok daha açık bir renk (Slate-700) */
|
||||||
|
border: 1px solid #475569 !important;
|
||||||
|
border-radius: 12px 12px 0 0 !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
border-bottom: none !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sütun Başlığı Metni */
|
||||||
|
.o_social_dashboard_kanban .o_kanban_header .o_column_title,
|
||||||
|
.o_social_dashboard_kanban .o_kanban_header span,
|
||||||
|
.o_social_dashboard_kanban .o_kanban_header div {
|
||||||
|
color: #ffffff !important;
|
||||||
|
font-weight: 700 !important;
|
||||||
|
font-size: 16px !important;
|
||||||
|
letter-spacing: 0.3px;
|
||||||
|
text-shadow: 0 1px 2px rgba(0,0,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sütun İçeriği (Kartların dizildiği yer) */
|
||||||
|
.o_social_dashboard_kanban .o_kanban_record_has_image_fill,
|
||||||
|
.o_social_dashboard_kanban .o_kanban_group > .o_kanban_record {
|
||||||
|
background: transparent !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 3. KARTLAR (POSTS) */
|
||||||
|
.o_social_dashboard_kanban .o_kanban_record {
|
||||||
|
background-color: #1e293b !important; /* Başlıkla aynı uyumlu renk */
|
||||||
|
border: 1px solid #334155 !important;
|
||||||
|
border-radius: 12px !important;
|
||||||
|
margin-top: 12px !important;
|
||||||
|
margin-bottom: 12px !important;
|
||||||
|
color: #e2e8f0 !important;
|
||||||
|
padding: 16px !important;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06) !important;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Kart Hover Efekti */
|
||||||
|
.o_social_dashboard_kanban .o_kanban_record:hover {
|
||||||
|
border-color: #64748b !important;
|
||||||
|
transform: translateY(-3px);
|
||||||
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.3), 0 4px 6px -2px rgba(0, 0, 0, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Yazar Bilgisi Alanı */
|
||||||
|
.o_social_author_section {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Yazar Avatarı */
|
||||||
|
.o_social_author_avatar {
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: linear-gradient(135deg, #3b82f6, #8b5cf6); /* Şık bir gradient */
|
||||||
|
margin-right: 12px;
|
||||||
|
box-shadow: 0 2px 4px rgba(0,0,0,0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_social_author_name {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #f1f5f9;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_social_time {
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post İçeriği (Mesaj) */
|
||||||
|
.o_social_message_text {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #cbd5e1;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-break: break-word;
|
||||||
|
flex-grow: 1; /* Mesaj kısmının esnek olması için */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alt Aksiyon Butonları (Beğeni, Yorum, Paylaş) */
|
||||||
|
.o_social_actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
color: #94a3b8;
|
||||||
|
font-size: 14px;
|
||||||
|
border-top: 1px solid #334155;
|
||||||
|
padding-top: 14px;
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_social_actions div {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_social_actions div:hover {
|
||||||
|
color: #38bdf8; /* Hover durumunda tatlı bir mavi */
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_social_actions i {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Odoo'nun Default Ekleme/Yükleme Butonlarını Gizle (Gerekirse) */
|
||||||
|
.o_social_dashboard_kanban .o_kanban_group .o_kanban_header .o_kanban_config {
|
||||||
|
color: #94a3b8 !important;
|
||||||
|
}
|
||||||
2
extra-addons/mymach_social_nextgen/tests/__init__.py
Normal file
2
extra-addons/mymach_social_nextgen/tests/__init__.py
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
from . import test_social_core
|
||||||
|
from . import test_social_auto_reply
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
|
||||||
|
class TestSocialAutoReply(TransactionCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(TestSocialAutoReply, cls).setUpClass()
|
||||||
|
# Test izolasyonu için mevcut tüm kuralları pasif yap
|
||||||
|
cls.env['mymach.social.auto.reply'].search([]).write({'active': False})
|
||||||
|
|
||||||
|
# Testler için gerekli verileri oluştur
|
||||||
|
cls.social_account = cls.env['mymach.social.account'].create({
|
||||||
|
'name': 'Test Account',
|
||||||
|
'platform': 'facebook',
|
||||||
|
'active': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
cls.stream = cls.env['mymach.social.stream'].create({
|
||||||
|
'name': 'Test Akışı',
|
||||||
|
'account_id': cls.social_account.id,
|
||||||
|
'stream_type': 'feed'
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_01_auto_reply_rule_creation(self):
|
||||||
|
"""Otomatik yanıt kuralı oluşturulabildiğini test et."""
|
||||||
|
rule = self.env['mymach.social.auto.reply'].create({
|
||||||
|
'name': 'Fiyat Kuralı',
|
||||||
|
'keyword': 'fiyat',
|
||||||
|
'reply_message': 'Fiyatlarımız için sitemize bakınız.',
|
||||||
|
'active': True
|
||||||
|
})
|
||||||
|
self.assertEqual(rule.keyword, 'fiyat')
|
||||||
|
self.assertEqual(rule.log_count, 0)
|
||||||
|
|
||||||
|
def test_02_auto_reply_trigger_and_log(self):
|
||||||
|
"""Bir gönderi oluşturulduğunda kuralın tetiklenip log oluşturduğunu test et."""
|
||||||
|
# 1. Kuralı oluştur
|
||||||
|
rule = self.env['mymach.social.auto.reply'].create({
|
||||||
|
'name': 'Fiyat Kuralı',
|
||||||
|
'keyword': 'fiyat',
|
||||||
|
'reply_message': 'Fiyatlarımız web sitemizdedir.',
|
||||||
|
'active': True
|
||||||
|
})
|
||||||
|
|
||||||
|
# Başlangıçta log yok
|
||||||
|
self.assertEqual(rule.log_count, 0)
|
||||||
|
|
||||||
|
# 2. 'fiyat' kelimesini içeren bir Stream Post oluştur
|
||||||
|
# Oluşturma işlemi (create) sırasında `mymach.social.stream.post` kuralı kontrol edip log atacak
|
||||||
|
post = self.env['mymach.social.stream.post'].create({
|
||||||
|
'stream_id': self.stream.id,
|
||||||
|
'author_name': 'Müşteri',
|
||||||
|
'message': 'Merhaba, ürünün fiyat listesini alabilir miyim?',
|
||||||
|
'message_type': 'comment'
|
||||||
|
})
|
||||||
|
|
||||||
|
# 3. Logun oluştuğunu doğrula
|
||||||
|
log = self.env['mymach.social.auto.reply.log'].search([('rule_id', '=', rule.id)])
|
||||||
|
self.assertEqual(len(log), 1)
|
||||||
|
|
||||||
|
rule.invalidate_recordset()
|
||||||
|
self.assertEqual(rule.log_count, 1)
|
||||||
|
|
||||||
|
self.assertEqual(log.status, 'success')
|
||||||
|
self.assertEqual(log.post_id.id, post.id)
|
||||||
|
self.assertEqual(log.trigger_keyword, 'fiyat')
|
||||||
|
|
||||||
|
def test_03_no_auto_reply_if_no_keyword(self):
|
||||||
|
"""Kural kelimesi eşleşmediğinde log oluşmadığını test et."""
|
||||||
|
rule = self.env['mymach.social.auto.reply'].create({
|
||||||
|
'name': 'Şikayet Kuralı',
|
||||||
|
'keyword': 'şikayet',
|
||||||
|
'reply_message': 'Lütfen destek hattını arayın.',
|
||||||
|
'active': True
|
||||||
|
})
|
||||||
|
|
||||||
|
# 'fiyat' geçen ama 'şikayet' geçmeyen mesaj
|
||||||
|
self.env['mymach.social.stream.post'].create({
|
||||||
|
'stream_id': self.stream.id,
|
||||||
|
'author_name': 'Müşteri 2',
|
||||||
|
'message': 'Sadece fiyat sormuştum.',
|
||||||
|
'message_type': 'comment'
|
||||||
|
})
|
||||||
|
|
||||||
|
self.assertEqual(rule.log_count, 0)
|
||||||
43
extra-addons/mymach_social_nextgen/tests/test_social_core.py
Normal file
43
extra-addons/mymach_social_nextgen/tests/test_social_core.py
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
from odoo.tests.common import TransactionCase
|
||||||
|
from odoo.exceptions import ValidationError
|
||||||
|
|
||||||
|
class TestSocialCore(TransactionCase):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(cls):
|
||||||
|
super(TestSocialCore, cls).setUpClass()
|
||||||
|
# Test hesabı oluştur
|
||||||
|
cls.social_account = cls.env['mymach.social.account'].create({
|
||||||
|
'name': 'Test Account',
|
||||||
|
'platform': 'facebook',
|
||||||
|
'active': True,
|
||||||
|
})
|
||||||
|
|
||||||
|
def test_01_social_account_creation(self):
|
||||||
|
"""Sosyal hesabın doğru ilişkilerle oluşturulduğunu test et."""
|
||||||
|
self.assertEqual(self.social_account.platform, 'facebook')
|
||||||
|
self.assertTrue(self.social_account.active)
|
||||||
|
|
||||||
|
def test_02_social_post_creation(self):
|
||||||
|
"""Sosyal gönderi oluşturma ve durum (state) doğrulama."""
|
||||||
|
post = self.env['mymach.social.post'].create({
|
||||||
|
'name': 'Test Gönderisi',
|
||||||
|
'content': 'Bu bir test gönderisidir.',
|
||||||
|
'account_ids': [(4, self.social_account.id)]
|
||||||
|
})
|
||||||
|
self.assertEqual(post.state, 'draft')
|
||||||
|
self.assertIn(self.social_account, post.account_ids)
|
||||||
|
|
||||||
|
# Post the message
|
||||||
|
post.action_publish()
|
||||||
|
self.assertEqual(post.state, 'published')
|
||||||
|
|
||||||
|
def test_03_social_stream_creation(self):
|
||||||
|
"""Sosyal akış (stream) oluşturmayı test et."""
|
||||||
|
stream = self.env['mymach.social.stream'].create({
|
||||||
|
'name': 'Test Akışı',
|
||||||
|
'account_id': self.social_account.id,
|
||||||
|
'stream_type': 'feed'
|
||||||
|
})
|
||||||
|
self.assertEqual(stream.name, 'Test Akışı')
|
||||||
|
self.assertEqual(stream.account_id.name, 'Test Account')
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="res_config_settings_view_form" model="ir.ui.view">
|
||||||
|
<field name="name">res.config.settings.view.form.inherit.social</field>
|
||||||
|
<field name="model">res.config.settings</field>
|
||||||
|
<field name="priority" eval="90"/>
|
||||||
|
<field name="inherit_id" ref="base.res_config_settings_view_form"/>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<xpath expr="//form" position="inside">
|
||||||
|
<app data-string="MyMach Social" string="MyMach Social" name="mymach_social_nextgen" groups="base.group_system">
|
||||||
|
<block title="Sosyal Medya API Kimlik Bilgileri" name="social_api_credentials_setting_container">
|
||||||
|
<setting string="Meta (Facebook/Instagram)" help="Meta Geliştirici Uygulaması için Client ID ve Secret">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt16">
|
||||||
|
<label for="social_meta_client_id" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_meta_client_id" class="oe_inline"/>
|
||||||
|
</div>
|
||||||
|
<div class="row mt8">
|
||||||
|
<label for="social_meta_client_secret" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_meta_client_secret" class="oe_inline" password="True"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="LinkedIn" help="LinkedIn Geliştirici Uygulaması için Client ID ve Secret">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt16">
|
||||||
|
<label for="social_linkedin_client_id" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_linkedin_client_id" class="oe_inline"/>
|
||||||
|
</div>
|
||||||
|
<div class="row mt8">
|
||||||
|
<label for="social_linkedin_client_secret" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_linkedin_client_secret" class="oe_inline" password="True"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="X (Twitter)" help="X Geliştirici Portalı için Client ID ve Secret">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt16">
|
||||||
|
<label for="social_x_client_id" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_x_client_id" class="oe_inline"/>
|
||||||
|
</div>
|
||||||
|
<div class="row mt8">
|
||||||
|
<label for="social_x_client_secret" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_x_client_secret" class="oe_inline" password="True"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="YouTube" help="Google Cloud Console için Client ID ve Secret">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt16">
|
||||||
|
<label for="social_youtube_client_id" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_youtube_client_id" class="oe_inline"/>
|
||||||
|
</div>
|
||||||
|
<div class="row mt8">
|
||||||
|
<label for="social_youtube_client_secret" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_youtube_client_secret" class="oe_inline" password="True"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
<block title="Yapay Zeka Asistan Ayarları" name="social_ai_settings_container">
|
||||||
|
<setting string="Google Gemini" help="Gemini Pro Entegrasyonu için API Anahtarı">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt16">
|
||||||
|
<label for="social_gemini_api_key" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_gemini_api_key" class="oe_inline" password="True"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="OpenAI (ChatGPT)" help="OpenAI modelleri için API Anahtarı">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt16">
|
||||||
|
<label for="social_openai_api_key" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_openai_api_key" class="oe_inline" password="True"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="Anthropic (Claude)" help="Claude AI modelleri için API Anahtarı">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt16">
|
||||||
|
<label for="social_claude_api_key" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_claude_api_key" class="oe_inline" password="True"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
<setting string="xAI (Grok)" help="Grok modelleri için API Anahtarı">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt16">
|
||||||
|
<label for="social_grok_api_key" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_grok_api_key" class="oe_inline" password="True"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
<block title="Medya Kütüphanesi & Filigran (Watermark) Ayarları" name="social_watermark_settings_container">
|
||||||
|
<setting string="Şirket Logosu (Filigran)" help="Üretilen veya yüklenen medyaların üzerine otomatik basılacak PNG formatında şeffaf logo.">
|
||||||
|
<div class="content-group">
|
||||||
|
<div class="row mt16">
|
||||||
|
<label for="social_watermark_image" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_watermark_image" widget="image" class="oe_avatar"/>
|
||||||
|
</div>
|
||||||
|
<div class="row mt8">
|
||||||
|
<label for="social_watermark_position" class="col-lg-3 o_light_label"/>
|
||||||
|
<field name="social_watermark_position" class="oe_inline"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</setting>
|
||||||
|
</block>
|
||||||
|
</app>
|
||||||
|
</xpath>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action for Settings -->
|
||||||
|
<record id="action_social_config_settings" model="ir.actions.act_window">
|
||||||
|
<field name="name">Ayarlar</field>
|
||||||
|
<field name="res_model">res.config.settings</field>
|
||||||
|
<field name="view_id" ref="res_config_settings_view_form"/>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="context">{'module' : 'mymach_social_nextgen', 'bin_size': False}</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<menuitem id="menu_social_settings"
|
||||||
|
name="Ayarlar"
|
||||||
|
parent="menu_social_configuration"
|
||||||
|
action="action_social_config_settings"
|
||||||
|
sequence="100"/>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Tree View -->
|
||||||
|
<record id="view_social_account_tree" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.account.list</field>
|
||||||
|
<field name="model">mymach.social.account</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Social Accounts">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="platform"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_social_account_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.account.form</field>
|
||||||
|
<field name="model">mymach.social.account</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Sosyal Medya Hesabı">
|
||||||
|
<header>
|
||||||
|
<button name="action_authenticate" string="Platforma Bağlan" type="object" class="oe_highlight" invisible="access_token != False"/>
|
||||||
|
<button name="action_refresh_token" string="Token Yenile" type="object" invisible="access_token == False"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="action_authenticate" type="object" class="oe_stat_button" icon="fa-users">
|
||||||
|
<field name="audience_count" widget="statinfo" string="Takipçiler"/>
|
||||||
|
</button>
|
||||||
|
<button name="action_authenticate" type="object" class="oe_stat_button" icon="fa-line-chart">
|
||||||
|
<field name="engagement_rate" widget="statinfo" string="Etkileşim %"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1><field name="name" placeholder="Hesap Adı..."/></h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="platform"/>
|
||||||
|
<field name="active"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="platform_account_id" invisible="platform_account_id == False"/>
|
||||||
|
<field name="token_expiration" invisible="access_token == False"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_social_account" model="ir.actions.act_window">
|
||||||
|
<field name="name">Sosyal Hesaplar</field>
|
||||||
|
<field name="res_model">mymach.social.account</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Create your first Social Media Account connection!
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Graph View -->
|
||||||
|
<record id="view_social_post_graph" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.post.graph</field>
|
||||||
|
<field name="model">mymach.social.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Sosyal Medya Analizleri" type="bar" sample="1">
|
||||||
|
<field name="scheduled_date" interval="month"/>
|
||||||
|
<field name="state"/>
|
||||||
|
<field name="likes_count" type="measure"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Pivot View for Analytics -->
|
||||||
|
<record id="view_social_post_pivot" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.post.pivot</field>
|
||||||
|
<field name="model">mymach.social.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<pivot string="Analiz Tablosu" sample="1">
|
||||||
|
<field name="scheduled_date" type="row" interval="month"/>
|
||||||
|
<field name="state" type="col"/>
|
||||||
|
<field name="likes_count" type="measure"/>
|
||||||
|
<field name="comments_count" type="measure"/>
|
||||||
|
<field name="shares_count" type="measure"/>
|
||||||
|
</pivot>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Sentiment Analysis Pie Chart -->
|
||||||
|
<record id="view_social_sentiment_graph" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.sentiment.graph</field>
|
||||||
|
<field name="model">mymach.social.stream.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Duygu Analizi" type="pie" sample="1">
|
||||||
|
<field name="sentiment"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_social_sentiment_dashboard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Duygu Analizi Panosu</field>
|
||||||
|
<field name="res_model">mymach.social.stream.post</field>
|
||||||
|
<field name="view_mode">graph,list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Müşteri Geri Bildirimleri Analizi
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Dashboard Action -->
|
||||||
|
<record id="action_social_dashboard" model="ir.actions.act_window">
|
||||||
|
<field name="name">Analiz Dashboard</field>
|
||||||
|
<field name="res_model">mymach.social.post</field>
|
||||||
|
<field name="view_mode">graph,pivot,kanban,list</field>
|
||||||
|
<field name="context">{'search_default_state': 'published'}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Sosyal Medya Performansınızı Takip Edin!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Gönderilerinizi yayınladıkça etkileşim grafikleriniz burada belirecektir.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
|
||||||
|
<!-- Log List (Tree) View -->
|
||||||
|
<record id="view_social_auto_reply_log_tree" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.auto.reply.log.tree</field>
|
||||||
|
<field name="model">mymach.social.auto.reply.log</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Otomatik Yanıt Geçmişi" decoration-success="status == 'success'" decoration-danger="status == 'failed'">
|
||||||
|
<field name="date" widget="datetime"/>
|
||||||
|
<field name="rule_id"/>
|
||||||
|
<field name="author_name"/>
|
||||||
|
<field name="account_id"/>
|
||||||
|
<field name="trigger_keyword"/>
|
||||||
|
<field name="status" widget="badge" decoration-success="status == 'success'" decoration-danger="status == 'failed'"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Log Form View -->
|
||||||
|
<record id="view_social_auto_reply_log_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.auto.reply.log.form</field>
|
||||||
|
<field name="model">mymach.social.auto.reply.log</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Otomatik Yanıt İşlemi">
|
||||||
|
<header>
|
||||||
|
<field name="status" widget="statusbar" statusbar_visible="success,failed"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="rule_id" readonly="1"/>
|
||||||
|
<field name="date" readonly="1"/>
|
||||||
|
<field name="account_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="author_name" readonly="1"/>
|
||||||
|
<field name="trigger_keyword" readonly="1"/>
|
||||||
|
<field name="post_id" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Gönderilen Yanıt">
|
||||||
|
<field name="sent_message" readonly="1"/>
|
||||||
|
</page>
|
||||||
|
<page string="Hata Mesajı" invisible="status == 'success'">
|
||||||
|
<field name="error_message" readonly="1"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Search View -->
|
||||||
|
<record id="view_social_auto_reply_log_search" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.auto.reply.log.search</field>
|
||||||
|
<field name="model">mymach.social.auto.reply.log</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<search>
|
||||||
|
<field name="rule_id"/>
|
||||||
|
<field name="author_name"/>
|
||||||
|
<field name="account_id"/>
|
||||||
|
|
||||||
|
<separator/>
|
||||||
|
<filter string="Başarılı" name="success" domain="[('status', '=', 'success')]"/>
|
||||||
|
<filter string="Başarısız" name="failed" domain="[('status', '=', 'failed')]"/>
|
||||||
|
|
||||||
|
<separator/>
|
||||||
|
<filter string="Kurala Göre Grupla" name="group_rule" context="{'group_by':'rule_id'}"/>
|
||||||
|
<filter string="Tarihe Göre Grupla" name="group_date" context="{'group_by':'date:day'}"/>
|
||||||
|
</search>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_social_auto_reply_log" model="ir.actions.act_window">
|
||||||
|
<field name="name">İşlem Geçmişi</field>
|
||||||
|
<field name="res_model">mymach.social.auto.reply.log</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="search_view_id" ref="view_social_auto_reply_log_search"/>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_social_auto_reply_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.auto.reply.form</field>
|
||||||
|
<field name="model">mymach.social.auto.reply</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Otomatik Yanıt Kuralı">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="action_view_logs" type="object" class="oe_stat_button" icon="fa-history" invisible="log_count == 0">
|
||||||
|
<field name="log_count" widget="statinfo" string="İşlemler"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<widget name="web_ribbon" title="Pasif" bg_color="bg-danger" invisible="active == True"/>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Kural Adı (Örn: Bilgi Talebi)"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group string="Kural Ayarları">
|
||||||
|
<field name="keyword" placeholder="bilgi"/>
|
||||||
|
<field name="active" widget="boolean_toggle"/>
|
||||||
|
<field name="sequence"/>
|
||||||
|
</group>
|
||||||
|
<group string="Filtreler">
|
||||||
|
<field name="apply_to_message_type" widget="radio" options="{'horizontal': true}"/>
|
||||||
|
<field name="account_ids" widget="many2many_tags" placeholder="Tüm hesaplar"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<group string="Yanıt İçeriği">
|
||||||
|
<field name="reply_message" nolabel="1" widget="text" placeholder="Gelecek olan mesajda bu kelime geçiyorsa otomatik olarak şu metni gönder..."/>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- List View -->
|
||||||
|
<record id="view_social_auto_reply_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.auto.reply.list</field>
|
||||||
|
<field name="model">mymach.social.auto.reply</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Otomatik Yanıt Kuralları">
|
||||||
|
<field name="sequence" widget="handle"/>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="keyword"/>
|
||||||
|
<field name="active" widget="boolean_toggle"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_social_auto_reply" model="ir.actions.act_window">
|
||||||
|
<field name="name">Otomatik Yanıt Kuralları</field>
|
||||||
|
<field name="res_model">mymach.social.auto.reply</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
İlk Otomatik Yanıt Kuralınızı Oluşturun!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Gelen yorum veya mesajlarda spesifik anahtar kelimeler ("bilgi", "fiyat" vb.) geçtiğinde sistemin otomatik yanıt vermesini sağlayabilirsiniz.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- List View -->
|
||||||
|
<record id="view_social_competitor_list" model="ir.ui.view">
|
||||||
|
<field name="name">social.competitor.list</field>
|
||||||
|
<field name="model">social.competitor</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Rakipler">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="platform"/>
|
||||||
|
<field name="followers_count"/>
|
||||||
|
<field name="engagement_rate"/>
|
||||||
|
<field name="last_scraped"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_social_competitor_form" model="ir.ui.view">
|
||||||
|
<field name="name">social.competitor.form</field>
|
||||||
|
<field name="model">social.competitor</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Rakip Bilgisi">
|
||||||
|
<header>
|
||||||
|
<button name="action_scrape_data" string="Verileri Kazı (Güncelle)" type="object" class="oe_highlight"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Rakip Firma Adı"/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="platform"/>
|
||||||
|
<field name="profile_url" widget="url"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="followers_count"/>
|
||||||
|
<field name="engagement_rate"/>
|
||||||
|
<field name="last_scraped"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Analiz Notları">
|
||||||
|
<field name="notes" placeholder="Stratejik notlar..."/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Kanban View -->
|
||||||
|
<record id="view_social_competitor_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">social.competitor.kanban</field>
|
||||||
|
<field name="model">social.competitor</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban class="o_kanban_dashboard">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="platform"/>
|
||||||
|
<field name="followers_count"/>
|
||||||
|
<field name="engagement_rate"/>
|
||||||
|
<template>
|
||||||
|
<t t-name="card">
|
||||||
|
<div class="oe_kanban_global_click">
|
||||||
|
<div class="o_kanban_record_title">
|
||||||
|
<strong><field name="name"/></strong>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<span class="badge rounded-pill text-bg-primary"><field name="platform"/></span>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2">
|
||||||
|
<i class="fa fa-users" title="Takipçiler"/> <field name="followers_count"/>
|
||||||
|
</div>
|
||||||
|
<div class="mt-1">
|
||||||
|
<i class="fa fa-line-chart" title="Etkileşim Oranı"/> <field name="engagement_rate"/> %
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Graph View -->
|
||||||
|
<record id="view_social_competitor_graph" model="ir.ui.view">
|
||||||
|
<field name="name">social.competitor.graph</field>
|
||||||
|
<field name="model">social.competitor</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<graph string="Rakip Takipçi Karşılaştırması" type="bar">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="followers_count" type="measure"/>
|
||||||
|
</graph>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_social_competitor" model="ir.actions.act_window">
|
||||||
|
<field name="name">Rakipler</field>
|
||||||
|
<field name="res_model">social.competitor</field>
|
||||||
|
<field name="view_mode">kanban,list,graph,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
İlk Rakibinizi Ekleyin!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Rakiplerinizin sosyal medya hesaplarını ekleyerek durumlarını izleyin.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Menu Item -->
|
||||||
|
<menuitem id="menu_social_competitors"
|
||||||
|
name="Rakipler"
|
||||||
|
parent="mymach_social_nextgen.menu_social_root"
|
||||||
|
action="action_social_competitor"
|
||||||
|
sequence="35"/>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<record id="view_social_live_post_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.live.post.list</field>
|
||||||
|
<field name="model">mymach.social.live.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Canlı Gönderiler" decoration-info="state == 'ready'" decoration-success="state == 'posted'" decoration-danger="state == 'failed'">
|
||||||
|
<field name="account_id"/>
|
||||||
|
<field name="post_id"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
<field name="likes_count"/>
|
||||||
|
<field name="comments_count"/>
|
||||||
|
<field name="shares_count"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_social_live_post" model="ir.actions.act_window">
|
||||||
|
<field name="name">Canlı Gönderiler</field>
|
||||||
|
<field name="res_model">mymach.social.live.post</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="domain">[('post_id', '=', active_id)]</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Media Form View -->
|
||||||
|
<record id="view_social_media_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.media.form</field>
|
||||||
|
<field name="model">mymach.social.media</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Medya Kütüphanesi">
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Medya Adı..."/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="media_type"/>
|
||||||
|
<field name="tag_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="attachment_id"/>
|
||||||
|
<field name="image_preview" widget="image" class="oe_avatar" invisible="media_type != 'image'"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Media List View -->
|
||||||
|
<record id="view_social_media_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.media.list</field>
|
||||||
|
<field name="model">mymach.social.media</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Medya Kütüphanesi">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="media_type"/>
|
||||||
|
<field name="tag_ids" widget="many2many_tags"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Media Kanban View -->
|
||||||
|
<record id="view_social_media_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.media.kanban</field>
|
||||||
|
<field name="model">mymach.social.media</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban class="o_kanban_mobile">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="media_type"/>
|
||||||
|
<field name="image_preview"/>
|
||||||
|
<field name="tag_ids"/>
|
||||||
|
<template>
|
||||||
|
<t t-name="card">
|
||||||
|
<div class="oe_kanban_global_click">
|
||||||
|
<div class="o_kanban_image">
|
||||||
|
<field name="image_preview" widget="image" options="{'size': [90, 90]}"/>
|
||||||
|
</div>
|
||||||
|
<div class="oe_kanban_details">
|
||||||
|
<strong><field name="name"/></strong>
|
||||||
|
<div><field name="media_type"/></div>
|
||||||
|
<div><field name="tag_ids" widget="many2many_tags"/></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Media Action -->
|
||||||
|
<record id="action_social_media" model="ir.actions.act_window">
|
||||||
|
<field name="name">Medya Kütüphanesi</field>
|
||||||
|
<field name="res_model">mymach.social.media</field>
|
||||||
|
<field name="view_mode">kanban,list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Medya Kütüphanenize ilk dosyanızı ekleyin!
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
139
extra-addons/mymach_social_nextgen/views/social_menu.xml
Normal file
139
extra-addons/mymach_social_nextgen/views/social_menu.xml
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Top Level Menu -->
|
||||||
|
<menuitem id="menu_social_root"
|
||||||
|
name="MyMach Social"
|
||||||
|
web_icon="mymach_social_nextgen,static/description/icon.png"
|
||||||
|
sequence="50"/>
|
||||||
|
|
||||||
|
<!-- Dashboard (Default Home) -->
|
||||||
|
<menuitem id="menu_social_dashboard"
|
||||||
|
name="Kontrol Paneli"
|
||||||
|
parent="menu_social_root"
|
||||||
|
action="action_social_dashboard_kanban"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
<!-- Calendar Action -->
|
||||||
|
<record id="action_social_calendar" model="ir.actions.act_window">
|
||||||
|
<field name="name">Takvim</field>
|
||||||
|
<field name="res_model">mymach.social.post</field>
|
||||||
|
<field name="view_mode">calendar,kanban,list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Aylık Gönderi Planınız Burada Görünecek!
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Sentiment Dashboard -->
|
||||||
|
<menuitem id="menu_social_sentiment"
|
||||||
|
name="Duygu Analizi"
|
||||||
|
parent="menu_social_root"
|
||||||
|
action="action_social_sentiment_dashboard"
|
||||||
|
sequence="12"/>
|
||||||
|
|
||||||
|
<!-- Inbox / Live Chat -->
|
||||||
|
<menuitem id="menu_social_inbox"
|
||||||
|
name="Gelen Kutusu (Sohbet)"
|
||||||
|
parent="menu_social_root"
|
||||||
|
action="action_social_inbox"
|
||||||
|
sequence="15"/>
|
||||||
|
|
||||||
|
<!-- Calendar Menu -->
|
||||||
|
<menuitem id="menu_social_calendar"
|
||||||
|
name="Takvim"
|
||||||
|
parent="menu_social_root"
|
||||||
|
action="action_social_calendar"
|
||||||
|
sequence="18"/>
|
||||||
|
|
||||||
|
<!-- Posts Menu -->
|
||||||
|
<menuitem id="menu_social_posts"
|
||||||
|
name="Gönderiler"
|
||||||
|
parent="menu_social_root"
|
||||||
|
action="action_social_post"
|
||||||
|
sequence="20"/>
|
||||||
|
|
||||||
|
<!-- Streams Menu -->
|
||||||
|
<menuitem id="menu_social_streams"
|
||||||
|
name="Akışlar (Streams)"
|
||||||
|
parent="menu_social_root"
|
||||||
|
action="action_social_stream"
|
||||||
|
sequence="25"/>
|
||||||
|
|
||||||
|
<!-- Campaigns Action -->
|
||||||
|
<record id="action_social_campaign" model="ir.actions.act_window">
|
||||||
|
<field name="name">Kampanyalar</field>
|
||||||
|
<field name="res_model">utm.campaign</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Pazarlama Kampanyalarınızı Buradan Yönetin
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Campaigns Menu -->
|
||||||
|
<menuitem id="menu_social_campaigns"
|
||||||
|
name="Kampanyalar"
|
||||||
|
parent="menu_social_root"
|
||||||
|
action="action_social_campaign"
|
||||||
|
sequence="30"/>
|
||||||
|
|
||||||
|
<!-- Media Library Menu -->
|
||||||
|
<menuitem id="menu_social_media_library"
|
||||||
|
name="Medya Kütüphanesi"
|
||||||
|
parent="menu_social_root"
|
||||||
|
action="action_social_media"
|
||||||
|
sequence="35"/>
|
||||||
|
|
||||||
|
<!-- Visitors Action -->
|
||||||
|
<record id="action_social_visitor" model="ir.actions.act_window">
|
||||||
|
<field name="name">Ziyaretçiler (Tıklamalar)</field>
|
||||||
|
<field name="res_model">link.tracker.click</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="context">{'create': False, 'edit': False}</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Sosyal Medya Ziyaretçilerinizi (Tıklamaları) Buradan Takip Edin
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Visitors Menu -->
|
||||||
|
<menuitem id="menu_social_visitors"
|
||||||
|
name="Ziyaretçiler"
|
||||||
|
parent="menu_social_root"
|
||||||
|
action="action_social_visitor"
|
||||||
|
sequence="40"/>
|
||||||
|
|
||||||
|
<!-- Configuration Menu -->
|
||||||
|
<menuitem id="menu_social_configuration"
|
||||||
|
name="Yapılandırma"
|
||||||
|
parent="menu_social_root"
|
||||||
|
sequence="100"/>
|
||||||
|
|
||||||
|
<!-- Accounts Menu (Under Configuration) -->
|
||||||
|
<menuitem id="menu_social_accounts_config"
|
||||||
|
name="Sosyal Hesaplar"
|
||||||
|
parent="menu_social_configuration"
|
||||||
|
action="action_social_account"
|
||||||
|
sequence="10"/>
|
||||||
|
|
||||||
|
<!-- Auto-Reply Rules Menu (Under Configuration) -->
|
||||||
|
<menuitem id="menu_social_auto_reply_config"
|
||||||
|
name="Otomatik Yanıt Kuralları"
|
||||||
|
parent="menu_social_configuration"
|
||||||
|
action="action_social_auto_reply"
|
||||||
|
sequence="20"/>
|
||||||
|
|
||||||
|
<!-- Auto-Reply Logs Menu (Under Configuration) -->
|
||||||
|
<menuitem id="menu_social_auto_reply_logs_config"
|
||||||
|
name="Otomatik Yanıt Geçmişi"
|
||||||
|
parent="menu_social_configuration"
|
||||||
|
action="action_social_auto_reply_log"
|
||||||
|
sequence="30"/>
|
||||||
|
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
144
extra-addons/mymach_social_nextgen/views/social_post_views.xml
Normal file
144
extra-addons/mymach_social_nextgen/views/social_post_views.xml
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Form View -->
|
||||||
|
<record id="view_social_post_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.post.form</field>
|
||||||
|
<field name="model">mymach.social.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Sosyal Medya Gönderisi">
|
||||||
|
<header>
|
||||||
|
<button name="action_open_ai_wizard" string="✨ AI ile İçerik Üret" type="object" class="btn-info" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_open_ai_image_wizard" string="🎨 AI Görsel Üret" type="object" class="btn-primary" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_request_approval" string="Onaya Gönder" type="object" invisible="state != 'draft'"/>
|
||||||
|
<button name="action_schedule" string="Planla" type="object" class="oe_highlight" invisible="state not in ('draft', 'waiting')" groups="mymach_social_nextgen.group_social_manager"/>
|
||||||
|
<button name="action_publish_now" string="Onayla ve Yayınla" type="object" class="btn-warning" invisible="state not in ('draft', 'waiting')" groups="mymach_social_nextgen.group_social_manager"/>
|
||||||
|
<button name="action_reset_to_draft" string="Taslağa Çevir" type="object" invisible="state == 'draft'" groups="mymach_social_nextgen.group_social_manager"/>
|
||||||
|
<field name="state" widget="statusbar" statusbar_visible="draft,waiting,scheduled,published"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<div class="oe_button_box" name="button_box">
|
||||||
|
<button name="%(action_social_live_post)d" type="action" class="oe_stat_button" icon="fa-share-square-o" invisible="state not in ('published', 'failed')">
|
||||||
|
<field name="live_post_ids" widget="statinfo" string="Canlı Gönderiler"/>
|
||||||
|
</button>
|
||||||
|
<button name="action_view_visitors" type="object" class="oe_stat_button" icon="fa-mouse-pointer" invisible="state not in ('published', 'failed')">
|
||||||
|
<field name="click_count" widget="statinfo" string="Ziyaretçiler"/>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="oe_title">
|
||||||
|
<h1>
|
||||||
|
<field name="name" placeholder="Gönderi Başlığı..."/>
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="account_ids" widget="many2many_tags" options="{'color_field': 'color'}"/>
|
||||||
|
<field name="scheduled_date" invisible="state == 'published'"/>
|
||||||
|
<field name="published_date" invisible="state != 'published'"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="media_ids" widget="many2many_binary" string="Görseller (Ekler)"/>
|
||||||
|
<field name="post_type" widget="radio" options="{'horizontal': true}"/>
|
||||||
|
<field name="utm_campaign_id"/>
|
||||||
|
<field name="product_id" string="E-Ticaret Ürünü" help="Seçilen ürünün satın alma linki ve fiyatı paylaşıma otomatik eklenecek."/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="İçerik">
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="content" widget="text" placeholder="Gönderinizi buraya yazın..." nolabel="1" colspan="2"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="preview_html" widget="html" nolabel="1" colspan="2" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
</page>
|
||||||
|
<page string="Hata Mesajı" invisible="state != 'failed'">
|
||||||
|
<field name="error_message"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<chatter/>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- List View -->
|
||||||
|
<record id="view_social_post_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.post.list</field>
|
||||||
|
<field name="model">mymach.social.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Sosyal Medya Gönderileri" decoration-info="state == 'scheduled'" decoration-success="state == 'published'" decoration-danger="state == 'failed'" decoration-warning="state == 'waiting'">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="account_ids" widget="many2many_tags"/>
|
||||||
|
<field name="scheduled_date"/>
|
||||||
|
<field name="published_date"/>
|
||||||
|
<field name="state" widget="badge"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Kanban View -->
|
||||||
|
<record id="view_social_post_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.post.kanban</field>
|
||||||
|
<field name="model">mymach.social.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban default_group_by="state" class="o_kanban_small_column" quick_create="false">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="state"/>
|
||||||
|
<field name="color"/>
|
||||||
|
<field name="scheduled_date"/>
|
||||||
|
<field name="account_ids"/>
|
||||||
|
<template>
|
||||||
|
<t t-name="card">
|
||||||
|
<div class="oe_kanban_global_click">
|
||||||
|
<div class="oe_kanban_content">
|
||||||
|
<div class="o_kanban_record_title">
|
||||||
|
<strong><field name="name"/></strong>
|
||||||
|
</div>
|
||||||
|
<div class="o_kanban_record_bottom mt-2">
|
||||||
|
<div class="oe_kanban_bottom_left">
|
||||||
|
<field name="account_ids" widget="many2many_tags"/>
|
||||||
|
</div>
|
||||||
|
<div class="oe_kanban_bottom_right">
|
||||||
|
<field name="scheduled_date"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Calendar View -->
|
||||||
|
<record id="view_social_post_calendar" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.post.calendar</field>
|
||||||
|
<field name="model">mymach.social.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<calendar string="Sosyal Medya Gönderileri" date_start="scheduled_date" color="state" mode="month" quick_create="0">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="account_ids"/>
|
||||||
|
<field name="state" filters="1"/>
|
||||||
|
</calendar>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_social_post" model="ir.actions.act_window">
|
||||||
|
<field name="name">Gönderiler</field>
|
||||||
|
<field name="res_model">mymach.social.post</field>
|
||||||
|
<field name="view_mode">kanban,list,calendar,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
İlk Sosyal Medya Gönderinizi Oluşturun!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
İçeriklerinizi hazırlayın, medyalarınızı ekleyin ve planlayın.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Social Dashboard Kanban View -->
|
||||||
|
<record id="view_social_stream_post_kanban_dashboard" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.stream.post.kanban.dashboard</field>
|
||||||
|
<field name="model">mymach.social.stream.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban class="o_social_dashboard_kanban" default_group_by="stream_id" create="0">
|
||||||
|
<field name="author_name"/>
|
||||||
|
<field name="published_date"/>
|
||||||
|
<field name="message"/>
|
||||||
|
<field name="likes_count"/>
|
||||||
|
<field name="comments_count"/>
|
||||||
|
<field name="shares_count"/>
|
||||||
|
<field name="stream_id"/>
|
||||||
|
|
||||||
|
<templates>
|
||||||
|
<t t-name="card">
|
||||||
|
<div t-attf-class="oe_kanban_global_click o_kanban_record">
|
||||||
|
<!-- Author & Time Info -->
|
||||||
|
<div class="o_social_author_section">
|
||||||
|
<div class="o_social_author_avatar d-flex align-items-center justify-content-center text-white">
|
||||||
|
<i class="fa fa-user"/>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-column">
|
||||||
|
<span class="o_social_author_name"><field name="author_name"/></span>
|
||||||
|
<span class="o_social_time"><field name="published_date"/></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Post Content -->
|
||||||
|
<div class="o_social_message_text">
|
||||||
|
<field name="message"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Actions / Metrics -->
|
||||||
|
<div class="o_social_actions">
|
||||||
|
<div title="Beğeniler">
|
||||||
|
<i class="fa fa-heart-o me-1"/> <field name="likes_count"/>
|
||||||
|
</div>
|
||||||
|
<div title="Yorumlar">
|
||||||
|
<i class="fa fa-comment-o me-1"/> <field name="comments_count"/>
|
||||||
|
</div>
|
||||||
|
<div title="Paylaşımlar">
|
||||||
|
<i class="fa fa-share-square-o me-1"/> <field name="shares_count"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Dashboard Action -->
|
||||||
|
<record id="action_social_dashboard_kanban" model="ir.actions.act_window">
|
||||||
|
<field name="name">Kontrol Paneli</field>
|
||||||
|
<field name="res_model">mymach.social.stream.post</field>
|
||||||
|
<field name="view_mode">kanban,form</field>
|
||||||
|
<field name="view_id" ref="view_social_stream_post_kanban_dashboard"/>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Harika Sosyal Medya Kontrol Panelinize Hoşgeldiniz!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Burada yapılandırdığınız akışlar, ekranınızda sütunlar halinde listelenecektir.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
148
extra-addons/mymach_social_nextgen/views/social_stream_views.xml
Normal file
148
extra-addons/mymach_social_nextgen/views/social_stream_views.xml
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<data>
|
||||||
|
<!-- Stream Kanban View -->
|
||||||
|
<record id="view_social_stream_kanban" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.stream.kanban</field>
|
||||||
|
<field name="model">mymach.social.stream</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<kanban class="o_kanban_dashboard" create="1">
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="account_id"/>
|
||||||
|
<field name="stream_type"/>
|
||||||
|
<field name="search_keyword"/>
|
||||||
|
<template>
|
||||||
|
<t t-name="card">
|
||||||
|
<div class="oe_kanban_global_click">
|
||||||
|
<div class="o_kanban_record_title">
|
||||||
|
<strong><field name="name"/></strong>
|
||||||
|
</div>
|
||||||
|
<div><field name="account_id"/></div>
|
||||||
|
<div><field name="stream_type"/></div>
|
||||||
|
<div t-if="record.search_keyword.raw_value" class="text-info">
|
||||||
|
<i class="fa fa-search" title="Aranan Kelime"/> <field name="search_keyword"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</template>
|
||||||
|
</kanban>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Stream Form View -->
|
||||||
|
<record id="view_social_stream_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.stream.form</field>
|
||||||
|
<field name="model">mymach.social.stream</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<field name="name"/>
|
||||||
|
<field name="account_id"/>
|
||||||
|
<field name="stream_type"/>
|
||||||
|
<field name="search_keyword" invisible="stream_type != 'keyword'" required="stream_type == 'keyword'"/>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Gönderiler ve Yorumlar">
|
||||||
|
<field name="stream_post_ids">
|
||||||
|
<list string="Akış Gönderileri">
|
||||||
|
<field name="author_name"/>
|
||||||
|
<field name="message"/>
|
||||||
|
<field name="likes_count"/>
|
||||||
|
<field name="comments_count"/>
|
||||||
|
<button name="action_create_lead" type="object" string="CRM Fırsatı Yarat" icon="fa-star" class="btn-success"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Action -->
|
||||||
|
<record id="action_social_stream" model="ir.actions.act_window">
|
||||||
|
<field name="name">Akışlar (Streams)</field>
|
||||||
|
<field name="res_model">mymach.social.stream</field>
|
||||||
|
<field name="view_mode">kanban,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
İlk Sosyal Medya Akışınızı Ekleyin!
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Sosyal medya hesaplarınıza gelen yorum ve mesajları buradan canlı takip edebilirsiniz.
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
<!-- Live Chat (Inbox) Action -->
|
||||||
|
<record id="action_social_inbox" model="ir.actions.act_window">
|
||||||
|
<field name="name">Canlı Sohbet (Gelen Kutusu)</field>
|
||||||
|
<field name="res_model">mymach.social.stream.post</field>
|
||||||
|
<field name="view_mode">list,form</field>
|
||||||
|
<field name="help" type="html">
|
||||||
|
<p class="o_view_nocontent_smiling_face">
|
||||||
|
Sosyal Medya Mesajlarınız Burada Toplanacak!
|
||||||
|
</p>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Live Chat List View -->
|
||||||
|
<record id="view_social_stream_post_list" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.stream.post.list</field>
|
||||||
|
<field name="model">mymach.social.stream.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<list string="Gelen Mesajlar" decoration-info="True" create="0">
|
||||||
|
<field name="published_date"/>
|
||||||
|
<field name="author_name" string="Gönderen"/>
|
||||||
|
<field name="account_id" string="Hesap"/>
|
||||||
|
<field name="message_type" widget="badge" decoration-info="message_type == 'direct_message'" decoration-muted="message_type == 'comment'"/>
|
||||||
|
<field name="message" string="Mesaj İçeriği"/>
|
||||||
|
<field name="sentiment" widget="badge" decoration-success="sentiment == 'positive'" decoration-danger="sentiment == 'negative'" decoration-warning="sentiment == 'neutral'"/>
|
||||||
|
<button name="action_analyze_sentiment" type="object" string="Duyguyu Analiz Et" icon="fa-smile-o"/>
|
||||||
|
<button name="action_create_lead" type="object" string="Fırsat Yarat" icon="fa-star" class="btn-success"/>
|
||||||
|
</list>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- Live Chat Form View -->
|
||||||
|
<record id="view_social_stream_post_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.stream.post.form</field>
|
||||||
|
<field name="model">mymach.social.stream.post</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Mesaj Detayı" create="0">
|
||||||
|
<header>
|
||||||
|
<button name="action_suggest_reply" type="object" string="✨ AI ile Yanıt Öner" class="btn-info"/>
|
||||||
|
<button name="action_analyze_sentiment" type="object" string="Duyguyu Analiz Et"/>
|
||||||
|
<button name="action_create_lead" type="object" string="CRM Fırsatı Yarat" class="oe_highlight"/>
|
||||||
|
</header>
|
||||||
|
<sheet>
|
||||||
|
<group>
|
||||||
|
<group>
|
||||||
|
<field name="author_name" readonly="1"/>
|
||||||
|
<field name="account_id" readonly="1"/>
|
||||||
|
<field name="published_date" readonly="1"/>
|
||||||
|
<field name="message_type" widget="badge" decoration-info="message_type == 'direct_message'" decoration-muted="message_type == 'comment'"/>
|
||||||
|
<field name="sentiment" widget="badge" decoration-success="sentiment == 'positive'" decoration-danger="sentiment == 'negative'" decoration-warning="sentiment == 'neutral'"/>
|
||||||
|
</group>
|
||||||
|
<group>
|
||||||
|
<field name="likes_count" readonly="1"/>
|
||||||
|
<field name="comments_count" readonly="1"/>
|
||||||
|
<field name="shares_count" readonly="1"/>
|
||||||
|
</group>
|
||||||
|
</group>
|
||||||
|
<notebook>
|
||||||
|
<page string="Mesaj İçeriği">
|
||||||
|
<field name="message" widget="text" readonly="1"/>
|
||||||
|
</page>
|
||||||
|
</notebook>
|
||||||
|
</sheet>
|
||||||
|
<div class="oe_chatter">
|
||||||
|
<field name="message_follower_ids"/>
|
||||||
|
<field name="message_ids"/>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</data>
|
||||||
|
</odoo>
|
||||||
3
extra-addons/mymach_social_nextgen/wizard/__init__.py
Normal file
3
extra-addons/mymach_social_nextgen/wizard/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
from . import social_ai_wizard
|
||||||
|
from . import social_ai_image_wizard
|
||||||
|
from . import social_ai_reply_wizard
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SocialAIImageWizard(models.TransientModel):
|
||||||
|
_name = 'mymach.social.ai.image.wizard'
|
||||||
|
_description = 'AI Image Generator Wizard'
|
||||||
|
|
||||||
|
post_id = fields.Many2one('mymach.social.post', string='Post', required=True)
|
||||||
|
image_prompt = fields.Text(string='Görsel Tarifi', required=True,
|
||||||
|
default='Örn: Masada duran şık bir kahve fincanı, arka planda gün batımı.')
|
||||||
|
|
||||||
|
def action_generate_image(self):
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
import urllib.parse
|
||||||
|
import base64
|
||||||
|
|
||||||
|
encoded_prompt = urllib.parse.quote(self.image_prompt)
|
||||||
|
|
||||||
|
# Pollinations AI generates images for free without API keys.
|
||||||
|
pollinations_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&nologo=true"
|
||||||
|
|
||||||
|
img_data = requests.get(pollinations_url, timeout=30).content
|
||||||
|
encoded_image = base64.b64encode(img_data)
|
||||||
|
|
||||||
|
media_record = self.env['ir.attachment'].create({
|
||||||
|
'name': f"AI_Generated_Image.png",
|
||||||
|
'datas': encoded_image,
|
||||||
|
'res_model': 'mymach.social.post',
|
||||||
|
'res_id': self.post_id.id,
|
||||||
|
'type': 'binary',
|
||||||
|
})
|
||||||
|
|
||||||
|
self.post_id.write({'media_ids': [(4, media_record.id)]})
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"Image Generation Error: {str(e)}")
|
||||||
|
raise UserError(f"Görsel üretilirken hata oluştu: {str(e)}")
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SocialAIReplyWizard(models.TransientModel):
|
||||||
|
_name = 'mymach.social.ai.reply.wizard'
|
||||||
|
_description = 'AI Reply Generator Wizard'
|
||||||
|
|
||||||
|
stream_post_id = fields.Many2one('mymach.social.stream.post', string='Mesaj', required=True)
|
||||||
|
customer_message = fields.Text(related='stream_post_id.message', string="Müşteri Mesajı")
|
||||||
|
suggested_reply = fields.Text(string='Üretilen Yanıt')
|
||||||
|
|
||||||
|
ai_provider = fields.Selection([
|
||||||
|
('gemini', 'Google Gemini'),
|
||||||
|
('openai', 'OpenAI (ChatGPT)'),
|
||||||
|
('claude', 'Anthropic (Claude)'),
|
||||||
|
('grok', 'xAI (Grok)')
|
||||||
|
], string='Yapay Zeka Sağlayıcısı', default='gemini', required=True)
|
||||||
|
|
||||||
|
def action_generate_reply(self):
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
system_instruction = "Sen profesyonel bir müşteri temsilcisisin. Gelen sosyal medya mesajına uygun, kibar, çözüm odaklı ve kurumsal bir yanıt önerisi oluştur. Kısa ve net ol."
|
||||||
|
user_prompt = f"Müşteri Mesajı: {self.customer_message}\n\nLütfen bu mesaja profesyonel bir yanıt üret."
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
generated_text = ""
|
||||||
|
|
||||||
|
if self.ai_provider == 'gemini':
|
||||||
|
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
|
||||||
|
if not api_key: raise UserError("Google Gemini API Anahtarı eksik!")
|
||||||
|
|
||||||
|
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
||||||
|
payload = {
|
||||||
|
"system_instruction": {"parts": [{"text": system_instruction}]},
|
||||||
|
"contents": [{"parts": [{"text": user_prompt}]}],
|
||||||
|
"generationConfig": {"temperature": 0.5, "maxOutputTokens": 200}
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload)
|
||||||
|
if response.status_code != 200: raise UserError(f"Gemini API Hatası: {response.text}")
|
||||||
|
generated_text = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '')
|
||||||
|
|
||||||
|
elif self.ai_provider == 'openai':
|
||||||
|
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.openai_api_key')
|
||||||
|
if not api_key: raise UserError("OpenAI API Anahtarı eksik!")
|
||||||
|
|
||||||
|
url = "https://api.openai.com/v1/chat/completions"
|
||||||
|
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
|
||||||
|
payload = {
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_instruction},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.5,
|
||||||
|
"max_tokens": 200
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
|
if response.status_code != 200: raise UserError(f"OpenAI API Hatası: {response.text}")
|
||||||
|
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||||
|
|
||||||
|
elif self.ai_provider == 'claude':
|
||||||
|
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.claude_api_key')
|
||||||
|
if not api_key: raise UserError("Claude API Anahtarı eksik!")
|
||||||
|
|
||||||
|
url = "https://api.anthropic.com/v1/messages"
|
||||||
|
headers = {
|
||||||
|
'x-api-key': api_key,
|
||||||
|
'anthropic-version': '2023-06-01',
|
||||||
|
'content-type': 'application/json'
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"model": "claude-3-5-sonnet-20240620",
|
||||||
|
"system": system_instruction,
|
||||||
|
"messages": [{"role": "user", "content": user_prompt}],
|
||||||
|
"max_tokens": 200,
|
||||||
|
"temperature": 0.5
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
|
if response.status_code != 200: raise UserError(f"Claude API Hatası: {response.text}")
|
||||||
|
generated_text = response.json().get('content', [{}])[0].get('text', '')
|
||||||
|
|
||||||
|
elif self.ai_provider == 'grok':
|
||||||
|
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.grok_api_key')
|
||||||
|
if not api_key: raise UserError("Grok API Anahtarı eksik!")
|
||||||
|
|
||||||
|
url = "https://api.x.ai/v1/chat/completions"
|
||||||
|
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
|
||||||
|
payload = {
|
||||||
|
"model": "grok-2",
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_instruction},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.5,
|
||||||
|
"max_tokens": 200
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
|
if response.status_code != 200: raise UserError(f"Grok API Hatası: {response.text}")
|
||||||
|
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||||
|
|
||||||
|
if not generated_text:
|
||||||
|
raise UserError(f"{self.ai_provider} boş bir yanıt döndürdü.")
|
||||||
|
|
||||||
|
self.suggested_reply = generated_text.strip()
|
||||||
|
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.act_window',
|
||||||
|
'res_model': 'mymach.social.ai.reply.wizard',
|
||||||
|
'res_id': self.id,
|
||||||
|
'view_mode': 'form',
|
||||||
|
'target': 'new',
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"AI API Error: {str(e)}")
|
||||||
|
raise UserError(f"AI ile iletişim kurulamadı: {str(e)}")
|
||||||
|
|
||||||
|
def action_send_reply(self):
|
||||||
|
self.ensure_one()
|
||||||
|
if not self.suggested_reply:
|
||||||
|
raise UserError("Gönderilecek bir yanıt yok.")
|
||||||
|
|
||||||
|
# In a real app, this would call the Meta Graph API to post the reply to the comment/message.
|
||||||
|
# For our NextGen simulation, we will just log it or simulate it.
|
||||||
|
# In this demo, let's just close the wizard and show a success notification.
|
||||||
|
return {
|
||||||
|
'type': 'ir.actions.client',
|
||||||
|
'tag': 'display_notification',
|
||||||
|
'params': {
|
||||||
|
'title': 'Başarılı',
|
||||||
|
'message': 'Mesaj başarıyla müşteriye iletildi!',
|
||||||
|
'type': 'success',
|
||||||
|
'sticky': False,
|
||||||
|
}
|
||||||
|
}
|
||||||
168
extra-addons/mymach_social_nextgen/wizard/social_ai_wizard.py
Normal file
168
extra-addons/mymach_social_nextgen/wizard/social_ai_wizard.py
Normal file
@@ -0,0 +1,168 @@
|
|||||||
|
from odoo import models, fields, api
|
||||||
|
from odoo.exceptions import UserError
|
||||||
|
import logging
|
||||||
|
|
||||||
|
_logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class SocialAIWizard(models.TransientModel):
|
||||||
|
_name = 'mymach.social.ai.wizard'
|
||||||
|
_description = 'AI Content Generator Wizard'
|
||||||
|
|
||||||
|
post_id = fields.Many2one('mymach.social.post', string='Post', required=True)
|
||||||
|
prompt = fields.Text(string='Gönderi Konusu', required=True,
|
||||||
|
default='Örn: Yeni çıkardığımız CNC makinesinin hız ve hassasiyet özelliklerini anlatan bir tanıtım gönderisi.')
|
||||||
|
tone = fields.Selection([
|
||||||
|
('professional', 'Profesyonel'),
|
||||||
|
('casual', 'Günlük ve Samimi'),
|
||||||
|
('humorous', 'Eğlenceli / Esprili'),
|
||||||
|
('formal', 'Resmi'),
|
||||||
|
], string='Ses Tonu', default='professional', required=True)
|
||||||
|
|
||||||
|
include_hashtags = fields.Boolean(string='Trend Hashtag\'ler Eklensin', default=True)
|
||||||
|
|
||||||
|
# Free Image Generation Fields
|
||||||
|
generate_image = fields.Boolean(string='Görsel Üret (Yapay Zeka - Ücretsiz)', default=False)
|
||||||
|
image_prompt = fields.Text(string='Görsel Tarifi', help='Eğer boş bırakılırsa, Gönderi Konusu kullanılacaktır.')
|
||||||
|
|
||||||
|
ai_provider = fields.Selection([
|
||||||
|
('gemini', 'Google Gemini'),
|
||||||
|
('openai', 'OpenAI (ChatGPT)'),
|
||||||
|
('claude', 'Anthropic (Claude)'),
|
||||||
|
('grok', 'xAI (Grok)'),
|
||||||
|
('none', 'Hiçbiri (Sadece Görsel Üret veya Manuel)')
|
||||||
|
], string='Yapay Zeka Sağlayıcısı', default='gemini', required=True)
|
||||||
|
|
||||||
|
def action_generate_content(self):
|
||||||
|
self.ensure_one()
|
||||||
|
|
||||||
|
# --- 1. Handle Text Generation if not 'none' ---
|
||||||
|
generated_text = ""
|
||||||
|
if self.ai_provider != 'none':
|
||||||
|
tone_map = dict(self._fields['tone'].selection).get(self.tone)
|
||||||
|
system_instruction = f"Sen uzman bir sosyal medya yöneticisisin. Verilen konuya göre Türkçe, etkileyici ve ilgi çekici bir sosyal medya gönderisi yaz. Tonlama: {tone_map}."
|
||||||
|
if self.include_hashtags:
|
||||||
|
system_instruction += " Metnin sonuna konuyla ilgili 3-5 adet trend hashtag ekle."
|
||||||
|
|
||||||
|
user_prompt = f"Konu: {self.prompt}\n\nLütfen bu konuya göre sosyal medya içeriğini üret."
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
import json
|
||||||
|
|
||||||
|
if self.ai_provider == 'gemini':
|
||||||
|
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.gemini_api_key')
|
||||||
|
if not api_key: raise UserError("Google Gemini API Anahtarı eksik!")
|
||||||
|
|
||||||
|
url = f"https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent?key={api_key}"
|
||||||
|
payload = {
|
||||||
|
"system_instruction": {"parts": [{"text": system_instruction}]},
|
||||||
|
"contents": [{"parts": [{"text": user_prompt}]}],
|
||||||
|
"generationConfig": {"temperature": 0.7, "maxOutputTokens": 400}
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers={'Content-Type': 'application/json'}, json=payload)
|
||||||
|
if response.status_code != 200: raise UserError(f"Gemini API Hatası: {response.text}")
|
||||||
|
generated_text = response.json().get('candidates', [{}])[0].get('content', {}).get('parts', [{}])[0].get('text', '')
|
||||||
|
|
||||||
|
elif self.ai_provider == 'openai':
|
||||||
|
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.openai_api_key')
|
||||||
|
if not api_key: raise UserError("OpenAI API Anahtarı eksik!")
|
||||||
|
|
||||||
|
url = "https://api.openai.com/v1/chat/completions"
|
||||||
|
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
|
||||||
|
payload = {
|
||||||
|
"model": "gpt-4o",
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_instruction},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tokens": 400
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
|
if response.status_code != 200: raise UserError(f"OpenAI API Hatası: {response.text}")
|
||||||
|
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||||
|
|
||||||
|
elif self.ai_provider == 'claude':
|
||||||
|
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.claude_api_key')
|
||||||
|
if not api_key: raise UserError("Claude API Anahtarı eksik!")
|
||||||
|
|
||||||
|
url = "https://api.anthropic.com/v1/messages"
|
||||||
|
headers = {
|
||||||
|
'x-api-key': api_key,
|
||||||
|
'anthropic-version': '2023-06-01',
|
||||||
|
'content-type': 'application/json'
|
||||||
|
}
|
||||||
|
payload = {
|
||||||
|
"model": "claude-3-5-sonnet-20240620",
|
||||||
|
"system": system_instruction,
|
||||||
|
"messages": [{"role": "user", "content": user_prompt}],
|
||||||
|
"max_tokens": 400,
|
||||||
|
"temperature": 0.7
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
|
if response.status_code != 200: raise UserError(f"Claude API Hatası: {response.text}")
|
||||||
|
generated_text = response.json().get('content', [{}])[0].get('text', '')
|
||||||
|
|
||||||
|
elif self.ai_provider == 'grok':
|
||||||
|
api_key = self.env['ir.config_parameter'].sudo().get_param('mymach_social.grok_api_key')
|
||||||
|
if not api_key: raise UserError("Grok API Anahtarı eksik!")
|
||||||
|
|
||||||
|
url = "https://api.x.ai/v1/chat/completions"
|
||||||
|
headers = {'Content-Type': 'application/json', 'Authorization': f'Bearer {api_key}'}
|
||||||
|
payload = {
|
||||||
|
"model": "grok-2",
|
||||||
|
"messages": [
|
||||||
|
{"role": "system", "content": system_instruction},
|
||||||
|
{"role": "user", "content": user_prompt}
|
||||||
|
],
|
||||||
|
"temperature": 0.7,
|
||||||
|
"max_tokens": 400
|
||||||
|
}
|
||||||
|
response = requests.post(url, headers=headers, json=payload)
|
||||||
|
if response.status_code != 200: raise UserError(f"Grok API Hatası: {response.text}")
|
||||||
|
generated_text = response.json().get('choices', [{}])[0].get('message', {}).get('content', '')
|
||||||
|
|
||||||
|
if not generated_text:
|
||||||
|
raise UserError(f"{self.ai_provider} boş bir yanıt döndürdü.")
|
||||||
|
generated_text = generated_text.strip()
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
_logger.error(f"AI API Error: {str(e)}")
|
||||||
|
raise UserError(f"AI ile iletişim kurulamadı: {str(e)}")
|
||||||
|
|
||||||
|
# --- 2. Generate Image (Free Stable Diffusion via Pollinations) ---
|
||||||
|
media_record = False
|
||||||
|
if self.generate_image:
|
||||||
|
import urllib.parse
|
||||||
|
import base64
|
||||||
|
import requests
|
||||||
|
|
||||||
|
img_prompt = self.image_prompt if self.image_prompt else self.prompt
|
||||||
|
encoded_prompt = urllib.parse.quote(img_prompt)
|
||||||
|
|
||||||
|
pollinations_url = f"https://image.pollinations.ai/prompt/{encoded_prompt}?width=1024&height=1024&nologo=true"
|
||||||
|
|
||||||
|
try:
|
||||||
|
img_data = requests.get(pollinations_url, timeout=30).content
|
||||||
|
encoded_image = base64.b64encode(img_data)
|
||||||
|
|
||||||
|
media_record = self.env['ir.attachment'].create({
|
||||||
|
'name': f"AI_Generated_Image.png",
|
||||||
|
'datas': encoded_image,
|
||||||
|
'res_model': 'mymach.social.post',
|
||||||
|
'res_id': self.post_id.id,
|
||||||
|
'type': 'binary',
|
||||||
|
})
|
||||||
|
except Exception as img_e:
|
||||||
|
_logger.error(f"Stable Diffusion Image Error: {str(img_e)}")
|
||||||
|
raise UserError(f"Ücretsiz görsel üretilirken hata oluştu: {str(img_e)}")
|
||||||
|
|
||||||
|
# --- 3. Update the post ---
|
||||||
|
update_vals = {}
|
||||||
|
if generated_text:
|
||||||
|
update_vals['content'] = generated_text
|
||||||
|
if media_record:
|
||||||
|
update_vals['media_ids'] = [(4, media_record.id)]
|
||||||
|
|
||||||
|
if update_vals:
|
||||||
|
self.post_id.write(update_vals)
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<odoo>
|
||||||
|
<record id="view_social_ai_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.ai.wizard.form</field>
|
||||||
|
<field name="model">mymach.social.ai.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Yapay Zeka İçerik Üreticisi">
|
||||||
|
<group>
|
||||||
|
<field name="post_id" invisible="1"/>
|
||||||
|
<field name="ai_provider"/>
|
||||||
|
<field name="prompt" invisible="ai_provider == 'none'"/>
|
||||||
|
<field name="tone" invisible="ai_provider == 'none'"/>
|
||||||
|
<field name="include_hashtags" invisible="ai_provider == 'none'"/>
|
||||||
|
</group>
|
||||||
|
<group string="Görsel Üretimi (Ücretsiz Yapay Zeka)">
|
||||||
|
<field name="generate_image" widget="boolean_toggle"/>
|
||||||
|
<field name="image_prompt" invisible="generate_image == False"/>
|
||||||
|
<div class="alert alert-info" role="alert" invisible="not generate_image">
|
||||||
|
Görseller <b>Ücretsiz (Stable Diffusion)</b> altyapısı ile üretilecektir. Herhangi bir API anahtarı veya ek bakiye gerekmez!
|
||||||
|
</div>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="action_generate_content" string="Üret (Generate)" type="object" class="btn-primary"/>
|
||||||
|
<button string="İptal" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<record id="action_social_ai_wizard" model="ir.actions.act_window">
|
||||||
|
<field name="name">AI ile İçerik Üret</field>
|
||||||
|
<field name="res_model">mymach.social.ai.wizard</field>
|
||||||
|
<field name="view_mode">form</field>
|
||||||
|
<field name="target">new</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- AI Image Wizard -->
|
||||||
|
<record id="view_social_ai_image_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.ai.image.wizard.form</field>
|
||||||
|
<field name="model">mymach.social.ai.image.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="Yapay Zeka Görsel Üreticisi">
|
||||||
|
<group>
|
||||||
|
<field name="post_id" invisible="1"/>
|
||||||
|
<field name="image_prompt"/>
|
||||||
|
</group>
|
||||||
|
<div class="alert alert-info" role="alert">
|
||||||
|
Görseller <b>Ücretsiz (Stable Diffusion)</b> altyapısı ile üretilecektir. Herhangi bir API anahtarı veya ek bakiye gerekmez!
|
||||||
|
</div>
|
||||||
|
<footer>
|
||||||
|
<button name="action_generate_image" string="Görsel Üret" type="object" class="btn-primary"/>
|
||||||
|
<button string="İptal" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
<!-- AI Reply Wizard -->
|
||||||
|
<record id="view_social_ai_reply_wizard_form" model="ir.ui.view">
|
||||||
|
<field name="name">mymach.social.ai.reply.wizard.form</field>
|
||||||
|
<field name="model">mymach.social.ai.reply.wizard</field>
|
||||||
|
<field name="arch" type="xml">
|
||||||
|
<form string="AI Yanıt Asistanı">
|
||||||
|
<group>
|
||||||
|
<field name="stream_post_id" invisible="1"/>
|
||||||
|
<field name="ai_provider"/>
|
||||||
|
<field name="customer_message" readonly="1"/>
|
||||||
|
<field name="suggested_reply"/>
|
||||||
|
</group>
|
||||||
|
<footer>
|
||||||
|
<button name="action_generate_reply" string="AI Yanıt Üret" type="object" class="btn-info" invisible="suggested_reply != False"/>
|
||||||
|
<button name="action_send_reply" string="Yanıtı Gönder" type="object" class="btn-success" invisible="not suggested_reply"/>
|
||||||
|
<button string="Kapat" class="btn-secondary" special="cancel"/>
|
||||||
|
</footer>
|
||||||
|
</form>
|
||||||
|
</field>
|
||||||
|
</record>
|
||||||
|
|
||||||
|
</odoo>
|
||||||
24
standalone-social-app/.gitignore
vendored
Normal file
24
standalone-social-app/.gitignore
vendored
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
node_modules
|
||||||
|
dist
|
||||||
|
dist-ssr
|
||||||
|
*.local
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
.idea
|
||||||
|
.DS_Store
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
8
standalone-social-app/.oxlintrc.json
Normal file
8
standalone-social-app/.oxlintrc.json
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"$schema": "./node_modules/oxlint/configuration_schema.json",
|
||||||
|
"plugins": ["react", "oxc"],
|
||||||
|
"rules": {
|
||||||
|
"react/rules-of-hooks": "error",
|
||||||
|
"react/only-export-components": ["warn", { "allowConstantExport": true }]
|
||||||
|
}
|
||||||
|
}
|
||||||
16
standalone-social-app/README.md
Normal file
16
standalone-social-app/README.md
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
# React + Vite
|
||||||
|
|
||||||
|
This template provides a minimal setup to get React working in Vite with HMR and some Oxlint rules.
|
||||||
|
|
||||||
|
Currently, two official plugins are available:
|
||||||
|
|
||||||
|
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||||
|
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||||
|
|
||||||
|
## React Compiler
|
||||||
|
|
||||||
|
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||||
|
|
||||||
|
## Expanding the Oxlint configuration
|
||||||
|
|
||||||
|
If you are developing a production application, we recommend using TypeScript with type-aware lint rules enabled. Check out the [TS template](https://github.com/vitejs/vite/tree/main/packages/create-vite/template-react-ts) for information on how to integrate TypeScript and Oxlint's TypeScript related rules in your project.
|
||||||
13
standalone-social-app/index.html
Normal file
13
standalone-social-app/index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>standalone-social-app</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root"></div>
|
||||||
|
<script type="module" src="/src/main.jsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
1396
standalone-social-app/package-lock.json
generated
Normal file
1396
standalone-social-app/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
24
standalone-social-app/package.json
Normal file
24
standalone-social-app/package.json
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"name": "standalone-social-app",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vite build",
|
||||||
|
"lint": "oxlint",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"lucide-react": "^1.27.0",
|
||||||
|
"react": "^19.2.7",
|
||||||
|
"react-dom": "^19.2.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/react": "^19.2.17",
|
||||||
|
"@types/react-dom": "^19.2.3",
|
||||||
|
"@vitejs/plugin-react": "^6.0.3",
|
||||||
|
"oxlint": "^1.71.0",
|
||||||
|
"vite": "^8.1.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
standalone-social-app/public/favicon.svg
Normal file
1
standalone-social-app/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
24
standalone-social-app/public/icons.svg
Normal file
24
standalone-social-app/public/icons.svg
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||||
|
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||||
|
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||||
|
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||||
|
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||||
|
</symbol>
|
||||||
|
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||||
|
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||||
|
</symbol>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.9 KiB |
184
standalone-social-app/src/App.css
Normal file
184
standalone-social-app/src/App.css
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
.counter {
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 5px 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
color: var(--accent);
|
||||||
|
background: var(--accent-bg);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
transition: border-color 0.3s;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border-color: var(--accent-border);
|
||||||
|
}
|
||||||
|
&:focus-visible {
|
||||||
|
outline: 2px solid var(--accent);
|
||||||
|
outline-offset: 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.hero {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.base,
|
||||||
|
.framework,
|
||||||
|
.vite {
|
||||||
|
inset-inline: 0;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.base {
|
||||||
|
width: 170px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework,
|
||||||
|
.vite {
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.framework {
|
||||||
|
z-index: 1;
|
||||||
|
top: 34px;
|
||||||
|
height: 28px;
|
||||||
|
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
|
||||||
|
scale(1.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
.vite {
|
||||||
|
z-index: 0;
|
||||||
|
top: 107px;
|
||||||
|
height: 26px;
|
||||||
|
width: auto;
|
||||||
|
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
|
||||||
|
scale(0.8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#center {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 25px;
|
||||||
|
place-content: center;
|
||||||
|
place-items: center;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
padding: 32px 20px 24px;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#next-steps {
|
||||||
|
display: flex;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
text-align: left;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
flex: 1 1 0;
|
||||||
|
padding: 32px;
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
padding: 24px 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#docs {
|
||||||
|
border-right: 1px solid var(--border);
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#next-steps ul {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
margin: 32px 0 0;
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: var(--text-h);
|
||||||
|
font-size: 16px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: var(--social-bg);
|
||||||
|
display: flex;
|
||||||
|
padding: 6px 12px;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: box-shadow 0.3s;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
.button-icon {
|
||||||
|
height: 18px;
|
||||||
|
width: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
margin-top: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
li {
|
||||||
|
flex: 1 1 calc(50% - 8px);
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
width: 100%;
|
||||||
|
justify-content: center;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#spacer {
|
||||||
|
height: 88px;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.ticks {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: -4.5px;
|
||||||
|
border: 5px solid transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
left: 0;
|
||||||
|
border-left-color: var(--border);
|
||||||
|
}
|
||||||
|
&::after {
|
||||||
|
right: 0;
|
||||||
|
border-right-color: var(--border);
|
||||||
|
}
|
||||||
|
}
|
||||||
33
standalone-social-app/src/App.jsx
Normal file
33
standalone-social-app/src/App.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import Sidebar from './components/Sidebar';
|
||||||
|
import Header from './components/Header';
|
||||||
|
import Dashboard from './components/Dashboard';
|
||||||
|
import PostModal from './components/PostModal';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
function App() {
|
||||||
|
const [activeTab, setActiveTab] = useState('dashboard');
|
||||||
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="app-container">
|
||||||
|
<Sidebar activeTab={activeTab} setActiveTab={setActiveTab} />
|
||||||
|
|
||||||
|
<main className="main-content">
|
||||||
|
<Header onNewPost={() => setIsModalOpen(true)} />
|
||||||
|
|
||||||
|
{activeTab === 'dashboard' || activeTab === 'feed' ? (
|
||||||
|
<Dashboard />
|
||||||
|
) : (
|
||||||
|
<div style={{ padding: '2rem', display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%', color: 'var(--text-muted)' }}>
|
||||||
|
<h2>{activeTab.charAt(0).toUpperCase() + activeTab.slice(1)} Module (Coming Soon)</h2>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</main>
|
||||||
|
|
||||||
|
{isModalOpen && <PostModal onClose={() => setIsModalOpen(false)} />}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default App;
|
||||||
BIN
standalone-social-app/src/assets/hero.png
Normal file
BIN
standalone-social-app/src/assets/hero.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
1
standalone-social-app/src/assets/react.svg
Normal file
1
standalone-social-app/src/assets/react.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||||
|
After Width: | Height: | Size: 4.0 KiB |
1
standalone-social-app/src/assets/vite.svg
Normal file
1
standalone-social-app/src/assets/vite.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.5 KiB |
128
standalone-social-app/src/components/Dashboard.jsx
Normal file
128
standalone-social-app/src/components/Dashboard.jsx
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { MoreHorizontal, MessageCircle, Heart, Share, BarChart2, Eye, MousePointerClick, Users } from 'lucide-react';
|
||||||
|
|
||||||
|
const StreamColumn = ({ title, platform, posts }) => {
|
||||||
|
return (
|
||||||
|
<div className="stream-column">
|
||||||
|
<div className="stream-header">
|
||||||
|
<div className="stream-title">
|
||||||
|
<div className={`platform-badge platform-${platform}`}>
|
||||||
|
<span style={{ fontSize: '0.8rem', fontWeight: 'bold' }}>
|
||||||
|
{platform.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span>{title}</span>
|
||||||
|
</div>
|
||||||
|
<button style={{ background: 'transparent', border: 'none', color: 'var(--text-muted)', cursor: 'pointer' }}>
|
||||||
|
<MoreHorizontal size={20} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="stream-content">
|
||||||
|
{posts.map(post => (
|
||||||
|
<div key={post.id} className="post-card">
|
||||||
|
<div className="post-header">
|
||||||
|
<div className="post-author">
|
||||||
|
<img src={post.avatar} alt="avatar" className="avatar" />
|
||||||
|
<div className="author-info">
|
||||||
|
<h4>{post.author}</h4>
|
||||||
|
<span>{post.time}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button style={{ background: 'transparent', border: 'none', color: 'var(--text-muted)', cursor: 'pointer' }}>
|
||||||
|
<MoreHorizontal size={16} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="post-text">{post.content}</p>
|
||||||
|
{post.image && <img src={post.image} alt="post media" className="post-image" />}
|
||||||
|
|
||||||
|
<div className="post-actions">
|
||||||
|
<button className="action-btn">
|
||||||
|
<Heart size={16} /> {post.likes}
|
||||||
|
</button>
|
||||||
|
<button className="action-btn">
|
||||||
|
<MessageCircle size={16} /> {post.comments}
|
||||||
|
</button>
|
||||||
|
<button className="action-btn">
|
||||||
|
<Share size={16} /> {post.shares}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Dashboard = () => {
|
||||||
|
const dummyPosts = [
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
author: 'Odoo Social',
|
||||||
|
time: '2 hours ago',
|
||||||
|
avatar: 'https://i.pravatar.cc/150?u=odoo',
|
||||||
|
content: 'Excited to announce our new upcoming features for the social marketing module! 🚀 #Odoo19 #Marketing',
|
||||||
|
image: 'https://images.unsplash.com/photo-1460925895917-afdab827c52f?ixlib=rb-4.0.3&auto=format&fit=crop&w=500&q=60',
|
||||||
|
likes: 124,
|
||||||
|
comments: 18,
|
||||||
|
shares: 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
author: 'Jane Doe',
|
||||||
|
time: '5 hours ago',
|
||||||
|
avatar: 'https://i.pravatar.cc/150?u=jane',
|
||||||
|
content: 'Just published a new blog post on how to optimize your digital campaigns.',
|
||||||
|
likes: 45,
|
||||||
|
comments: 3,
|
||||||
|
shares: 12
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||||
|
<div className="metric-cards">
|
||||||
|
<div className="metric-card">
|
||||||
|
<div className="metric-icon"><Users size={24} /></div>
|
||||||
|
<div className="metric-info">
|
||||||
|
<h3>24.5k</h3>
|
||||||
|
<p>Total Audience</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="metric-card">
|
||||||
|
<div className="metric-icon"><Eye size={24} /></div>
|
||||||
|
<div className="metric-info">
|
||||||
|
<h3>142.1k</h3>
|
||||||
|
<p>Impressions</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="metric-card">
|
||||||
|
<div className="metric-icon"><MousePointerClick size={24} /></div>
|
||||||
|
<div className="metric-info">
|
||||||
|
<h3>12.4k</h3>
|
||||||
|
<p>Engagements</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="metric-card">
|
||||||
|
<div className="metric-icon"><BarChart2 size={24} /></div>
|
||||||
|
<div className="metric-info">
|
||||||
|
<h3>8.2%</h3>
|
||||||
|
<p>Engagement Rate</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="dashboard-scroll">
|
||||||
|
<div className="streams-container">
|
||||||
|
<StreamColumn title="Company Page" platform="facebook" posts={dummyPosts} />
|
||||||
|
<StreamColumn title="Official Twitter" platform="twitter" posts={[dummyPosts[0]]} />
|
||||||
|
<StreamColumn title="LinkedIn Corporate" platform="linkedin" posts={dummyPosts} />
|
||||||
|
<StreamColumn title="Instagram Feed" platform="instagram" posts={[dummyPosts[1]]} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
33
standalone-social-app/src/components/Header.jsx
Normal file
33
standalone-social-app/src/components/Header.jsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Plus, Bell, Search, User } from 'lucide-react';
|
||||||
|
|
||||||
|
const Header = ({ onNewPost }) => {
|
||||||
|
return (
|
||||||
|
<header className="topbar">
|
||||||
|
<div className="search-bar" style={{ display: 'flex', alignItems: 'center', background: 'var(--bg-primary)', padding: '0.5rem 1rem', borderRadius: '2rem', border: '1px solid var(--border-color)', width: '300px' }}>
|
||||||
|
<Search size={18} color="var(--text-muted)" style={{ marginRight: '0.5rem' }} />
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search posts, campaigns..."
|
||||||
|
style={{ background: 'transparent', border: 'none', color: 'var(--text-primary)', outline: 'none', width: '100%' }}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="user-menu">
|
||||||
|
<button className="btn btn-primary" onClick={onNewPost}>
|
||||||
|
<Plus size={18} />
|
||||||
|
New Post
|
||||||
|
</button>
|
||||||
|
<div style={{ position: 'relative', cursor: 'pointer' }}>
|
||||||
|
<Bell size={20} color="var(--text-secondary)" />
|
||||||
|
<span style={{ position: 'absolute', top: '-2px', right: '-2px', background: 'var(--accent-primary)', width: '8px', height: '8px', borderRadius: '50%' }}></span>
|
||||||
|
</div>
|
||||||
|
<div className="avatar" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'var(--accent-light)', color: 'var(--accent-primary)', cursor: 'pointer' }}>
|
||||||
|
<User size={20} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Header;
|
||||||
95
standalone-social-app/src/components/PostModal.jsx
Normal file
95
standalone-social-app/src/components/PostModal.jsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { X, Image as ImageIcon, Link as LinkIcon, Calendar, Check } from 'lucide-react';
|
||||||
|
|
||||||
|
const PostModal = ({ onClose }) => {
|
||||||
|
const [message, setMessage] = useState('');
|
||||||
|
const [platforms, setPlatforms] = useState({
|
||||||
|
facebook: true,
|
||||||
|
twitter: true,
|
||||||
|
linkedin: false,
|
||||||
|
instagram: false
|
||||||
|
});
|
||||||
|
|
||||||
|
const togglePlatform = (platform) => {
|
||||||
|
setPlatforms(prev => ({ ...prev, [platform]: !prev[platform] }));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlePost = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// Simulate posting
|
||||||
|
setTimeout(() => {
|
||||||
|
onClose();
|
||||||
|
}, 600);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="modal-overlay" onClick={onClose}>
|
||||||
|
<div className="modal-content" onClick={e => e.stopPropagation()}>
|
||||||
|
<div className="modal-header">
|
||||||
|
<h2 style={{ fontSize: '1.25rem', fontWeight: 600 }}>Create New Post</h2>
|
||||||
|
<button onClick={onClose} style={{ background: 'transparent', border: 'none', color: 'var(--text-secondary)', cursor: 'pointer' }}>
|
||||||
|
<X size={24} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form onSubmit={handlePost}>
|
||||||
|
<div className="modal-body">
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Select Platforms</label>
|
||||||
|
<div className="platform-select">
|
||||||
|
{['facebook', 'twitter', 'linkedin', 'instagram'].map(platform => (
|
||||||
|
<div
|
||||||
|
key={platform}
|
||||||
|
className={`platform-checkbox ${platforms[platform] ? 'selected' : ''}`}
|
||||||
|
onClick={() => togglePlatform(platform)}
|
||||||
|
>
|
||||||
|
<div className={`platform-badge platform-${platform}`}>
|
||||||
|
{/* Using first letter as a simple logo replacement for now */}
|
||||||
|
<span style={{ fontSize: '0.8rem', fontWeight: 'bold' }}>
|
||||||
|
{platform.charAt(0).toUpperCase()}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span style={{ fontSize: '0.875rem', textTransform: 'capitalize' }}>{platform}</span>
|
||||||
|
{platforms[platform] && <Check size={16} color="var(--accent-primary)" style={{ marginLeft: 'auto' }} />}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Message</label>
|
||||||
|
<textarea
|
||||||
|
className="form-control"
|
||||||
|
placeholder="What do you want to share?"
|
||||||
|
value={message}
|
||||||
|
onChange={e => setMessage(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex gap-4">
|
||||||
|
<button type="button" className="action-btn">
|
||||||
|
<ImageIcon size={18} />
|
||||||
|
<span>Attach Image</span>
|
||||||
|
</button>
|
||||||
|
<button type="button" className="action-btn">
|
||||||
|
<LinkIcon size={18} />
|
||||||
|
<span>Add Link</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="modal-footer">
|
||||||
|
<button type="button" className="btn btn-secondary" onClick={onClose}>Cancel</button>
|
||||||
|
<button type="button" className="btn btn-secondary flex items-center gap-2">
|
||||||
|
<Calendar size={16} /> Schedule
|
||||||
|
</button>
|
||||||
|
<button type="submit" className="btn btn-primary">Post Now</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostModal;
|
||||||
48
standalone-social-app/src/components/Sidebar.jsx
Normal file
48
standalone-social-app/src/components/Sidebar.jsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
LayoutDashboard,
|
||||||
|
Share2,
|
||||||
|
Calendar,
|
||||||
|
MessageCircle,
|
||||||
|
Users,
|
||||||
|
Settings,
|
||||||
|
Activity
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
const Sidebar = ({ activeTab, setActiveTab }) => {
|
||||||
|
const navItems = [
|
||||||
|
{ id: 'dashboard', label: 'Dashboard', icon: LayoutDashboard },
|
||||||
|
{ id: 'feed', label: 'Feed', icon: Activity },
|
||||||
|
{ id: 'posts', label: 'Posts', icon: Share2 },
|
||||||
|
{ id: 'calendar', label: 'Calendar', icon: Calendar },
|
||||||
|
{ id: 'visitors', label: 'Visitors', icon: Users },
|
||||||
|
{ id: 'chat', label: 'Live Chat', icon: MessageCircle },
|
||||||
|
{ id: 'settings', label: 'Configuration', icon: Settings },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<aside className="sidebar">
|
||||||
|
<div className="brand">
|
||||||
|
<div className="brand-icon">
|
||||||
|
<Share2 size={24} color="white" />
|
||||||
|
</div>
|
||||||
|
<span>Odoo Social</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav className="nav-menu">
|
||||||
|
{navItems.map((item) => (
|
||||||
|
<div
|
||||||
|
key={item.id}
|
||||||
|
className={`nav-item ${activeTab === item.id ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab(item.id)}
|
||||||
|
>
|
||||||
|
<item.icon size={20} />
|
||||||
|
<span>{item.label}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
482
standalone-social-app/src/index.css
Normal file
482
standalone-social-app/src/index.css
Normal file
@@ -0,0 +1,482 @@
|
|||||||
|
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg-primary: #0f172a;
|
||||||
|
--bg-secondary: #1e293b;
|
||||||
|
--bg-tertiary: #334155;
|
||||||
|
--text-primary: #f8fafc;
|
||||||
|
--text-secondary: #cbd5e1;
|
||||||
|
--text-muted: #94a3b8;
|
||||||
|
|
||||||
|
--accent-primary: #6366f1;
|
||||||
|
--accent-hover: #4f46e5;
|
||||||
|
--accent-light: rgba(99, 102, 241, 0.15);
|
||||||
|
|
||||||
|
--border-color: rgba(255, 255, 255, 0.1);
|
||||||
|
--glass-bg: rgba(30, 41, 59, 0.7);
|
||||||
|
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgba(0, 0, 0, 0.05);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
|
||||||
|
|
||||||
|
--radius-md: 0.5rem;
|
||||||
|
--radius-lg: 0.75rem;
|
||||||
|
--radius-xl: 1rem;
|
||||||
|
|
||||||
|
--transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', sans-serif;
|
||||||
|
background-color: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
line-height: 1.5;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Layout */
|
||||||
|
.app-container {
|
||||||
|
display: flex;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sidebar */
|
||||||
|
.sidebar {
|
||||||
|
width: 260px;
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 1.5rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.brand-icon {
|
||||||
|
background: linear-gradient(135deg, var(--accent-primary), #a855f7);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
margin-bottom: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-item:hover, .nav-item.active {
|
||||||
|
background: var(--accent-light);
|
||||||
|
color: var(--accent-primary);
|
||||||
|
transform: translateX(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Header */
|
||||||
|
.topbar {
|
||||||
|
height: 72px;
|
||||||
|
background: var(--glass-bg);
|
||||||
|
backdrop-filter: blur(12px);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 2rem;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
padding: 0.5rem 1.25rem;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary {
|
||||||
|
background: linear-gradient(135deg, var(--accent-primary), #a855f7);
|
||||||
|
color: white;
|
||||||
|
box-shadow: 0 4px 14px 0 rgba(99, 102, 241, 0.39);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-primary:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-secondary:hover {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard & Streams */
|
||||||
|
.dashboard-scroll {
|
||||||
|
flex: 1;
|
||||||
|
overflow-x: auto;
|
||||||
|
overflow-y: hidden;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.streams-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 1.5rem;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-column {
|
||||||
|
width: 380px;
|
||||||
|
min-width: 380px;
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-header {
|
||||||
|
padding: 1.25rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-content {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stream-content::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
.stream-content::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Post Cards */
|
||||||
|
.post-card {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 1.25rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-card:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: var(--text-muted);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-author {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar {
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-info h4 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-info span {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-text {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-image {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding-top: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: transparent;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-btn:hover {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Modal */
|
||||||
|
.modal-overlay {
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
backdrop-filter: blur(4px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 100;
|
||||||
|
animation: fadeIn 0.2s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-content {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
width: 100%;
|
||||||
|
max-width: 600px;
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
animation: slideUp 0.3s ease-out;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
padding: 1.5rem;
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group label {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 0.75rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
font-family: inherit;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-control:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 0 0 2px var(--accent-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
textarea.form-control {
|
||||||
|
min-height: 120px;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Utils */
|
||||||
|
.flex { display: flex; }
|
||||||
|
.items-center { align-items: center; }
|
||||||
|
.justify-between { justify-content: space-between; }
|
||||||
|
.gap-2 { gap: 0.5rem; }
|
||||||
|
.gap-4 { gap: 1rem; }
|
||||||
|
.text-xs { font-size: 0.75rem; }
|
||||||
|
.text-sm { font-size: 0.875rem; }
|
||||||
|
.text-muted { color: var(--text-muted); }
|
||||||
|
|
||||||
|
/* Animations */
|
||||||
|
@keyframes fadeIn {
|
||||||
|
from { opacity: 0; }
|
||||||
|
to { opacity: 1; }
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideUp {
|
||||||
|
from { opacity: 0; transform: translateY(20px); }
|
||||||
|
to { opacity: 1; transform: translateY(0); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* User Profile Mini */
|
||||||
|
.user-menu {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
padding: 0 2rem;
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.25rem;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card:hover {
|
||||||
|
transform: translateY(-3px);
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 10px 25px -5px rgba(99, 102, 241, 0.15);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-icon {
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-info h3 {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-info p {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.platform-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
border-radius: 50%;
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
.platform-facebook { background-color: #1877F2; }
|
||||||
|
.platform-twitter { background-color: #1DA1F2; }
|
||||||
|
.platform-linkedin { background-color: #0A66C2; }
|
||||||
|
.platform-instagram { background: linear-gradient(45deg, #f09433 0%, #e6683c 25%, #dc2743 50%, #cc2366 75%, #bc1888 100%); }
|
||||||
|
|
||||||
|
.platform-select {
|
||||||
|
display: flex;
|
||||||
|
gap: 1rem;
|
||||||
|
margin-bottom: 1.5rem;
|
||||||
|
}
|
||||||
|
.platform-checkbox {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition);
|
||||||
|
}
|
||||||
|
.platform-checkbox:hover {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
.platform-checkbox.selected {
|
||||||
|
background: var(--accent-light);
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
}
|
||||||
10
standalone-social-app/src/main.jsx
Normal file
10
standalone-social-app/src/main.jsx
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
import { StrictMode } from 'react'
|
||||||
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import './index.css'
|
||||||
|
import App from './App.jsx'
|
||||||
|
|
||||||
|
createRoot(document.getElementById('root')).render(
|
||||||
|
<StrictMode>
|
||||||
|
<App />
|
||||||
|
</StrictMode>,
|
||||||
|
)
|
||||||
7
standalone-social-app/vite.config.js
Normal file
7
standalone-social-app/vite.config.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { defineConfig } from 'vite'
|
||||||
|
import react from '@vitejs/plugin-react'
|
||||||
|
|
||||||
|
// https://vite.dev/config/
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [react()],
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user